Firefox-Marionette-1.63/0000755000175000017500000000000014763402247013603 5ustar davedaveFirefox-Marionette-1.63/lib/0000755000175000017500000000000014763402246014350 5ustar davedaveFirefox-Marionette-1.63/lib/Firefox/0000755000175000017500000000000014763402246015752 5ustar davedaveFirefox-Marionette-1.63/lib/Firefox/Marionette/0000755000175000017500000000000014763402246020061 5ustar davedaveFirefox-Marionette-1.63/lib/Firefox/Marionette/GeoLocation.pm0000644000175000017500000002523414763400572022630 0ustar davedavepackage Firefox::Marionette::GeoLocation; use strict; use warnings; use Encode(); use overload q[""] => '_lat_long'; use charnames qw(:full); our $VERSION = '1.63'; sub _MINUTES_IN_ONE_HOUR { return 60 } sub _MINUTES_IN_ONE_DEGREE { return 60 } sub _SECONDS_IN_ONE_MINUTE { return 60 } sub _NUMBER_TO_ADD_PRIOR_TO_ROUNDING { return 0.5 } sub _NEGATIVE_OFFSET { return -1 } my $latitude_code = 'lat'; my $longitude_code = 'lng'; sub new { my ( $class, @parameters ) = @_; my %parameters; if ( ( scalar @parameters ) == 1 ) { %parameters = %{ $parameters[0] }; } else { %parameters = @parameters; } my $self = bless {}, $class; my %mappings = ( latitude => $latitude_code, longitude => $longitude_code, long => $longitude_code, lon => $longitude_code, altitude_accuracy => 'altitudeAccuracy', timeZone => 'timezone_offset', countryCode => 'country_code', country_code2 => 'country_code', ); my %keys = ( lng => 1, lat => 1, altitude => 1, accuracy => 1, heading => 1, speed => 1, altitudeAccuracy => 1, speed => 1, timezone_offset => 1, timezone_name => 1, country_code => 1, ); foreach my $original ( sort { $a cmp $b } keys %parameters ) { if ( exists $mappings{$original} ) { $self->{ $mappings{$original} } = $parameters{$original}; } elsif ( $keys{$original} ) { $self->{$original} = $parameters{$original}; } } if ( defined $self->{timezone_offset} ) { if ( $self->{timezone_offset} =~ /^([+-])(\d{1,2}):(\d{1,2})$/smx ) { $self->{timezone_offset} = $self->_calculate_offset_for_javascript( $1, $2, $3 ); } } elsif ( defined $parameters{time_zone}{current_time} ) { if ( $parameters{time_zone}{current_time} =~ /([+-])(\d{2})(\d{2})$/smx ) { $self->{timezone_offset} = $self->_calculate_offset_for_javascript( $1, $2, $3 ); } } if ( defined $parameters{time_zone}{name} ) { $self->{tz} = $parameters{time_zone}{name}; } if ( defined $self->{country_code} ) { if ( $self->{country_code} !~ /^[[:upper:]]{2}$/smx ) { delete $self->{country_code}; } } return $self; } sub _calculate_offset_for_javascript { my ( $self, $sign, $hours, $minutes ) = @_; my $offset = $hours * _MINUTES_IN_ONE_HOUR() + $minutes; if ( $sign ne q[-] ) { $offset *= _NEGATIVE_OFFSET(); } return $offset; } sub _lat_long { my ($self) = @_; # https://www.wikihow.com/Write-Latitude-and-Longitude my $lat_direction = $self->latitude() >= 0 ? q[N] : q[S]; my $lng_direction = $self->longitude() >= 0 ? q[E] : q[W]; my $lat_degrees = int abs $self->latitude(); my $lng_degrees = int abs $self->longitude(); my $lat_minutes = int( ( ( abs $self->latitude() ) - $lat_degrees ) * _MINUTES_IN_ONE_DEGREE() ); my $lng_minutes = int( ( ( abs $self->longitude() ) - $lng_degrees ) * _MINUTES_IN_ONE_DEGREE() ); my $lat_seconds = int( ( ( ( ( ( abs $self->latitude() ) - $lat_degrees ) * _MINUTES_IN_ONE_DEGREE() ) - $lat_minutes ) * _SECONDS_IN_ONE_MINUTE() ) + _NUMBER_TO_ADD_PRIOR_TO_ROUNDING() ); my $lng_seconds = int( ( ( ( ( ( abs $self->longitude() ) - $lng_degrees ) * _MINUTES_IN_ONE_DEGREE() ) - $lng_minutes ) * _SECONDS_IN_ONE_MINUTE() ) + _NUMBER_TO_ADD_PRIOR_TO_ROUNDING() ); return "$lat_degrees\N{DEGREE SIGN}$lat_minutes'$lat_seconds\"$lat_direction,$lng_degrees\N{DEGREE SIGN}$lng_minutes'$lng_seconds\"$lng_direction"; } sub TO_JSON { my ($self) = @_; my $json = {}; foreach my $key ( sort { $a cmp $b } keys %{$self} ) { if ( $key =~ /^(?:$longitude_code|$latitude_code)$/smx ) { $json->{location}->{$key} = $self->{$key}; } elsif ( $key =~ /^(?:accuracy|altitude)$/smx ) { $json->{$key} = $self->{$key}; } } return $json; } sub latitude { my ($self) = @_; return $self->{$latitude_code}; } sub longitude { my ($self) = @_; return $self->{$longitude_code}; } sub altitude { my ($self) = @_; return $self->{altitude}; } sub accuracy { my ($self) = @_; return $self->{accuracy}; } sub altitude_accuracy { my ($self) = @_; return $self->{altitudeAccuracy}; } sub heading { my ($self) = @_; return $self->{heading}; } sub speed { my ($self) = @_; return $self->{speed}; } sub timezone_offset { my ($self) = @_; return $self->{timezone_offset}; } sub tz { my ($self) = @_; return $self->{tz}; } sub country_code { my ($self) = @_; return $self->{country_code}; } sub uri { my ($self) = @_; my $uri = q[geo:] . join q[,], $self->latitude(), $self->longitude(), ( defined $self->altitude() ? $self->altitude() : () ); if ( defined $self->accuracy() ) { $uri = join q[;], $uri, q[u=] . $self->accuracy(); } return URI->new($uri); } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::GeoLocation - Represents a GeoLocation for Firefox =head1 VERSION Version 1.63 =head1 SYNOPSIS use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(geo => { lat => -37.814, lng => 144.96332 }; ... =head1 DESCRIPTION This module provides an easy interface for the L object in Firefox =head1 SUBROUTINES/METHODS =head2 accuracy returns the accuracy of the L and L properties, expressed in meters. =head2 altitude returns the position's altitude in meters, relative to nominal sea level. This value may not be defined. =head2 altitude_accuracy returns the accuracy of the altitude expressed in meters. This value may not be defined. =head2 country_code returns the country_code (L) of the location. This value may not be defined. =head2 heading returns the direction towards which the device is facing. This value, specified in degrees, indicates how far off from heading true north the device is. 0 degrees represents true north, and the direction is determined clockwise (which means that east is 90 degrees and west is 270 degrees). This value may not be defined. =head2 latitude returns the position's latitude in decimal degrees. =head2 longitude returns the position's longitude in decimal degrees. =head2 new accepts an optional hash as a parameter. Allowed keys are below; =over 4 =item * accuracy - the accuracy of the L and L properties, expressed in meters. =item * altitude - the accuracy of the altitude expressed in meters. =item * altitude_accuracy - accuracy of the altitude expressed in meters. =item * heading - the direction towards which the device is facing. This value, specified in degrees, indicates how far off from heading true north the device is. 0 degrees represents true north, and the direction is determined clockwise (which means that east is 90 degrees and west is 270 degrees). =item * lat - see latitude. =item * latitude - the position's latitude in decimal degrees. =item * lon - see longitude. =item * long - see longitude. =item * longitude - the position's longitude in decimal degrees. =item * lng - see longitude. =item * speed - the velocity of the device in meters per second. =item * tz - the timezone as an L. =back This method returns a new C object. =head2 speed returns the velocity of the device in meters per second. This value may not be defined. =head2 timezone_offset returns the timezone offset in minutes from GMT. This value may not be defined. =head2 tz returns the timezone as an L. This value may not be defined. =head2 TO_JSON required to allow L to work correctly. This method should not need to be called directly. =head2 uri This method returns the object encoded as a new L. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::GeoLocation 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) 2024, 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.63/lib/Firefox/Marionette/Exception/0000755000175000017500000000000014763402246022017 5ustar davedaveFirefox-Marionette-1.63/lib/Firefox/Marionette/Exception/InsecureCertificate.pm0000644000175000017500000000554014763400572026301 0ustar davedavepackage Firefox::Marionette::Exception::InsecureCertificate; use strict; use warnings; use parent qw(Firefox::Marionette::Exception::Response); our $VERSION = '1.63'; 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.63 =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) 2024, 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.63/lib/Firefox/Marionette/Exception/Response.pm0000755000175000017500000000713514763400572024164 0ustar davedavepackage Firefox::Marionette::Exception::Response; use strict; use warnings; use parent qw(Firefox::Marionette::Exception); our $VERSION = '1.63'; 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.63 =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) 2024, 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.63/lib/Firefox/Marionette/Exception/NoSuchAlert.pm0000755000175000017500000000547414763400572024561 0ustar davedavepackage Firefox::Marionette::Exception::NoSuchAlert; use strict; use warnings; use parent qw(Firefox::Marionette::Exception::Response); our $VERSION = '1.63'; 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.63 =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) 2024, 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.63/lib/Firefox/Marionette/Exception/NotFound.pm0000755000175000017500000000574214763400572024124 0ustar davedavepackage Firefox::Marionette::Exception::NotFound; use strict; use warnings; use parent qw(Firefox::Marionette::Exception::Response); our $VERSION = '1.63'; 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.63 =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) 2024, 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.63/lib/Firefox/Marionette/Exception/StaleElement.pm0000755000175000017500000000562514763400572024752 0ustar davedavepackage Firefox::Marionette::Exception::StaleElement; use strict; use warnings; use parent qw(Firefox::Marionette::Exception::Response); our $VERSION = '1.63'; sub throw { my ( $class, $response, $parameters ) = @_; my $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.63 =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) 2024, 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.63/lib/Firefox/Marionette/Proxy.pm0000755000175000017500000002172614763400572021553 0ustar davedavepackage Firefox::Marionette::Proxy; use strict; use warnings; our $VERSION = '1.63'; sub DEFAULT_SOCKS_VERSION { return 5 } sub DEFAULT_SQUID_PORT { return 3128 } 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[:] . DEFAULT_SQUID_PORT(); } $parameters{http} = $host; $parameters{https} = $host; } elsif ( $parameters{tls} ) { $parameters{pac} = $class->get_inline_pac( 'https://' . $parameters{tls} ); } else { if ( $parameters{socks} ) { $parameters{type} = 'manual'; if ( !defined $parameters{socks_version} ) { $parameters{socks_version} = DEFAULT_SOCKS_VERSION(); } } } my $element = bless {%parameters}, $class; return $element; } sub get_inline_pac { my ( $class, @proxies ) = @_; my $body = join q[;], map { ( uc $_->scheme() eq 'HTTP' ? 'PROXY' : uc $_->scheme() ) . q[ ] . $_->host_port() } map { URI->new($_) } @proxies; return qq[data:text/plain,function FindProxyForURL(){return "$body"}]; } 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.63 =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 ) ); # OR my $proxy = Firefox::Marionette::Proxy->new( tls => "squid.example.org:443" ); 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 CONSTANTS =head2 DEFAULT_SOCKS_VERSION returns the default SOCKS version which is 5. =head2 DEFAULT_SQUID_PORT returns the L =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'. If the port is not specified it defaults to L, which is 3128. =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'. =item * tls - defines a L function pointing to a TLS secured proxy for FTP, HTTP, and HTTPS traffic. This was derived from L =back This method returns a new L object. =head2 get_inline_pac returns a L file for the parameters supplied to this function. This is only intended for internal use. =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 L proxy traffic when the L is 'manual'. =head2 socks_version returns the SOCKS proxy version when the L is 'manual'. =head1 SETTING UP SOCKS SERVERS USING SSH You can setup a simple SOCKS proxy with L, using the L<-D option|https://man.openbsd.org/ssh#D>. If you setup such a server with the following command ssh -ND localhost:8080 user@Remote.Proxy.Server and then connect to it like so; my $firefox = Firefox::Marionette->new( proxy => Firefox::Marionette::Proxy->new(socks => 'localhost:8080') )->go('https://Target.Web.Site'); the following network diagram describes what will happen ------------ ---------- ---------- | Firefox | SSH | Remote | HTTPS | Target | | & Perl |--------->| Proxy |-------->| Web | | run here | | Server | | Site | ------------ ---------- ---------- =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) 2024, 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.63/lib/Firefox/Marionette/LocalObject.pm0000644000175000017500000001031414763400572022577 0ustar davedavepackage Firefox::Marionette::LocalObject; use strict; use warnings; use overload q[""] => 'uuid', 'cmp' => '_cmp', q[==] => '_numeric_eq'; our $VERSION = '1.63'; sub new { my ( $class, $browser, %parameters ) = @_; my $window = bless { browser => $browser, %parameters }, $class; return $window; } sub TO_JSON { my ($self) = @_; my $json = {}; if ( $self->{_old_protocols_key} ) { $json->{ $self->{_old_protocols_key} } = $self->uuid(); } else { $json->{ $self->IDENTIFIER() } = $self->uuid(); } return $json; } sub browser { my ($self) = @_; return $self->{browser}; } sub uuid { my ($self) = @_; return $self->{ $self->IDENTIFIER() }; } sub _cmp { my ( $a, $b ) = @_; return $a->uuid() cmp $b->uuid(); } sub _numeric_eq { my ( $a, $b ) = @_; return $a->uuid() == $b->uuid(); } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::LocalObject - Parent class that represents a Firefox local object retrieved using the Marionette protocol =head1 VERSION Version 1.63 =head1 SYNOPSIS use Firefox::Marionette(); use Cwd(); my $path = File::Spec->catfile(Cwd::cwd(), qw(t data elements.html)); my $firefox = Firefox::Marionette->new()->go("file://$path"); # getting a reference to a browser object in perl my $span = $firefox->has_tag('span'); # working on that referenced object back in the browser my $child = $firefox->script('return arguments[0].children[0]', args => [ $span ]); =head1 DESCRIPTION This module handles the implementation of a Firefox L using the Marionette protocol. It is here to provide a single place for common methods for this type of object and should not be instantiated directly. =head1 SUBROUTINES/METHODS =head2 browser returns the L connected with this object. =head2 new returns a new object. =head2 TO_JSON required to allow L to work correctly. This method should not need to be called directly. =head2 uuid returns the browser generated id connected with this object. The id is usually a UUID, but may not be, especially for older versions of Firefox =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::LocalObject 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) 2024, 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.63/lib/Firefox/Marionette/Element.pm0000755000175000017500000007216314763400572022024 0ustar davedavepackage Firefox::Marionette::Element; use strict; use warnings; use parent qw(Firefox::Marionette::LocalObject); our $VERSION = '1.63'; 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}; } return $class->SUPER::new( $browser, %parameters ); } sub aria_label { my ($self) = @_; return $self->browser()->aria_label($self); } sub aria_role { my ($self) = @_; return $self->browser()->aria_role($self); } sub click { my ($self) = @_; return $self->browser()->click($self); } sub clear { my ($self) = @_; return $self->browser()->clear($self); } sub scroll { my ( $self, $arguments ) = @_; return $self->browser()->scroll( $self, $arguments ); } 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.63 =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 CONSTANTS =head2 IDENTIFIER returns the L =head1 SUBROUTINES/METHODS =head2 aria_label returns the L for the L. =head2 aria_role returns the L for the L. =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 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('page-content'); $div->find('//input[@id="metacpan_search-input"]')->type('Test::More'); # OR in list context my $div = $firefox->find_class('page-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('page-content'); $div->find_id('metacpan_search-input')->type('Test::More'); # OR in list context my $div = $firefox->find_class('page-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('page-content'); $div->find_name('q')->type('Test::More'); # OR in list context my $div = $firefox->find_class('page-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('page-content'); $div->find_class('form-control home-metacpan_search-input')->type('Test::More'); # OR in list context my $div = $firefox->find_class('page-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('page-content'); $div->find_selector('input.home-metacpan_search-input')->type('Test::More'); # OR in list context my $div = $firefox->find_class('page-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('page-content'); my $input = $div->find_tag('input'); # OR in list context my $div = $firefox->find_class('page-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('footer-links'); $div->find_link('API')->click(); # OR in list context my $div = $firefox->find_class('footer-links'); 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('footer-links'); $div->find_partial('AP')->click(); # OR in list context my $div = $firefox->find_class('footer-links'); 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('page-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('page-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('page-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('page-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('page-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('page-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('footer-links'); 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('footer-links'); 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 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 scroll accepts an optional parameter which is the same as for the L method. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(visible => 1)->go('https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView'); $firefox->find_id('content')->find_link('Examples')->scroll({ behavior => 'smooth', block => 'center' }); =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) 2024, 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.63/lib/Firefox/Marionette/WebFrame.pm0000644000175000017500000000566614763400572022124 0ustar davedavepackage Firefox::Marionette::WebFrame; use strict; use warnings; use parent qw(Firefox::Marionette::LocalObject); our $VERSION = '1.63'; sub IDENTIFIER { return 'frame-075b-4da1-b6ba-e579c2d3230a' } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::WebFrame - Represents a Firefox web frame retrieved using the Marionette protocol =head1 VERSION Version 1.63 =head1 SYNOPSIS use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(); # load a webpage with frames my $javascript_frame = $firefox->script("return window.frames[0]"); =head1 DESCRIPTION This module handles the implementation of a Firefox Web Frame using the Marionette protocol =head1 CONSTANTS =head2 IDENTIFIER returns the L =head1 SUBROUTINES/METHODS =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::WebFrame 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) 2024, 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.63/lib/Firefox/Marionette/Bookmark.pm0000644000175000017500000003120414763400572022164 0ustar davedavepackage Firefox::Marionette::Bookmark; use strict; use warnings; use URI::URL(); use URI::data(); use Exporter(); *import = \&Exporter::import; our @EXPORT_OK = qw( BOOKMARK FOLDER MENU MOBILE ROOT SEPARATOR TAGS TOOLBAR UNFILED ); our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK, ); our $VERSION = '1.63'; # guids from toolkit/components/places/Bookmarks.sys.mjs sub MENU { return 'menu________' } sub MOBILE { return 'mobile______' } sub ROOT { return 'root________' } sub TAGS { return 'tags________'; } # With bug 424160, tags will stop being bookmarks sub TOOLBAR { return 'toolbar_____' } sub UNFILED { return 'unfiled_____' } # types from toolkit/components/places/nsINavBookmarksService.idl sub BOOKMARK { return 1 } sub FOLDER { return 2 } sub SEPARATOR { return 3 } my %mapping = ( guid => 'guid', keyword => 'keyword', url => 'url', title => 'title', type => 'type', tags => 'tags', index => 'index', parent_guid => 'parentGuid', date_added => 'dateAdded', last_modified => 'lastModified', icon_url => 'iconUrl', icon => 'icon', ); sub new { my ( $class, %parameters ) = @_; my $bookmark = bless {}, $class; foreach my $name ( qw(parent_guid guid url title date_added last_modified type icon icon_url index tags keyword) ) { if ( defined $parameters{$name} ) { $bookmark->{$name} = $parameters{$name}; } if ( defined $parameters{ $mapping{$name} } ) { $bookmark->{$name} = $parameters{ $mapping{$name} }; } } foreach my $name (qw(url icon_url)) { if ( defined $bookmark->{$name} ) { $bookmark->{$name} = URI::URL->new( $bookmark->{$name} ); } } foreach my $name (qw(icon)) { if ( defined $bookmark->{$name} ) { $bookmark->{$name} = URI::data->new( $bookmark->{$name} ); } } if ( !defined $bookmark->{type} ) { if ( defined $bookmark->{url} ) { $bookmark->{type} = BOOKMARK(); } elsif ( defined $bookmark->{title} ) { $bookmark->{type} = FOLDER(); } } if ( ( defined $bookmark->{guid} ) && ( $bookmark->{guid} eq ROOT() ) ) { } else { $bookmark->{parent_guid} = $bookmark->{parent_guid} ? $bookmark->{parent_guid} : MENU(); } return $bookmark; } sub TO_JSON { my ($self) = @_; my $json = {}; foreach my $key ( sort { $a cmp $b } keys %{$self} ) { if ( ( $key eq 'url' ) || ( $key eq 'icon_url' ) || ( $key eq 'icon' ) ) { $json->{ $mapping{$key} } = $self->{$key}->as_string(); } else { $json->{ $mapping{$key} } = $self->{$key}; } if ( ( $key eq 'date_added' ) || ( $key eq 'last_modified' ) ) { $json->{ $mapping{$key} } =~ s/000$//smx; } } return $json; } sub url { my ($self) = @_; return $self->{url}; } sub title { my ($self) = @_; return $self->{title}; } sub guid { my ($self) = @_; return $self->{guid}; } sub type { my ($self) = @_; return $self->{type}; } sub content_type { my ($self) = @_; if ( my $type = $self->type() ) { if ( $type == BOOKMARK() ) { return 'text/x-moz-place'; } elsif ( $type == FOLDER() ) { return 'text/x-moz-place-container'; } elsif ( $type == SEPARATOR() ) { return 'text/x-moz-place-separator'; } } return; } sub parent_guid { my ($self) = @_; return $self->{parent_guid}; } sub date_added { my ($self) = @_; return $self->{date_added}; } sub last_modified { my ($self) = @_; return $self->{last_modified}; } sub idx { my ($self) = @_; return $self->{index}; } sub icon { my ($self) = @_; return $self->{icon}; } sub icon_url { my ($self) = @_; return $self->{icon_url}; } sub tags { my ($self) = @_; if ( defined $self->{tags} ) { return @{ $self->{tags} }; } else { return (); } } sub keyword { my ($self) = @_; return $self->{keyword}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Bookmark - Represents a Firefox bookmark retrieved using the Marionette protocol =head1 VERSION Version 1.63 =head1 SYNOPSIS use Firefox::Marionette(); use Firefox::Marionette::Bookmark qw(:all); use Encode(); use v5.10; my $firefox = Firefox::Marionette->new(); $firefox->import_bookmarks("/path/to/bookmarks.html"); foreach my $bookmark (reverse $firefox->bookmarks()) { say "Bookmark guid is :" . $bookmark->guid(); say "Bookmark parent guid is :" . $bookmark->parent_guid(); say "Bookmark date added is :" . localtime($bookmark->date_added()); say "Bookmark last modified is :" . localtime($bookmark->last_modified()); say "Bookmark index is :" . $bookmark->idx(); if ($bookmark->type() == BOOKMARK()) { say "Bookmark url :" . $bookmark->url(); say "Bookmark title is :" . Encode::encode('UTF-8', $bookmark->title(), 1) if ($bookmark->title()); say "Bookmark icon is :" . $bookmark->icon() if ($bookmark->icon()); say "Bookmark icon url is :" . $bookmark->icon_url() if ($bookmark->icon_url()); say "Bookmark keyword is :" . Encode::encode('UTF-8', $bookmark->keyword(), 1) if ($bookmark->keyword()); say "Bookmark tags are :" . Encode::encode('UTF-8', (join q[, ], $bookmark->tags())) if ($bookmark->tags()); } elsif ($bookmark->type() == FOLDER()) { given ($bookmark->guid()) { when (MENU() . q[]) { say "This is the menu folder" } when (ROOT() . q[]) { say "This is the root folder" } when (TAGS() . q[]) { say "This is the tags folder" } when (TOOLBAR() . q[]) { say "This is the toolbar folder" } when (UNFILED() . q[]) { say "This is the unfiled folder" } when (MOBILE() . q[]) { say "This is the mobile folder" } default { say "Folder title is :" . $bookmark->title() } } } else { say "-" x 50; } } =head1 DESCRIPTION This module handles the implementation of a single Firefox bookmark using the Marionette protocol. =head1 CONSTANTS Constants are sourced from L. =head2 ROOT returns the guid of the root of the bookmark hierarchy. This is equal to the string 'root________'. =head2 MENU return the guid for the menu folder. This is equal to the string 'menu________'. =head2 TAGS return the guid for the tags folder. This is equal to the string 'tags________'. With L, tags will stop being bookmarks. =head2 TOOLBAR return the guid for the toolbar folder. This is equal to the string 'toolbar_____'. =head2 UNFILED return the guid for the unfiled folder. This is equal to the string 'unfiled_____'. =head2 MOBILE return the guid for the mobile folder. This is equal to the string 'mobile______'. =head2 BOOKMARK returns the integer 1. =head2 FOLDER returns the integer 2. =head2 SEPARATOR returns the integer 3. =head1 SUBROUTINES/METHODS =head2 new accepts a hash as a parameter. Allowed keys are below; =over 4 =item * date_added. the time the bookmark was added in seconds since the UNIX epoch. =item * icon - the favicon for the bookmark. It should be encoded as a L. =item * icon_url - the url for the bookmark favicon. It should be encoded as a L. =item * index - the index of the bookmark. This describes the bookmark's position in the hierarchy. =item * guid - the unique identifier for the bookmark in Firefox. This key is optional. If the a bookmark is saved without a guid, firefox will generate a guid automatically. =item * last_modified - the time the bookmark was last modified in seconds since the UNIX epoch. =item * parent_guid - the guid of the parent folder in the bookmark hierarchy. The default parent guid will be the L. =item * title - the title of the bookmark. This can be a L name or a L title. =item * type - an integer describing the type of this object. It can be a L, a L or a L. =item * url - the L of the bookmark. Only bookmarks with a type of L will have a L set. =back This method returns a new L object. =head2 content_type returns the content type of the bookmark (for example 'text/x-moz-place-container' for a folder). =head2 date_added returns the time the bookmark was added in seconds since the UNIX epoch. =head2 icon returns the favicon of the bookmark if known. It will be returned as a L object. =head2 icon_url returns the URL of the favicon of the bookmark if known. It will be returned as a L object. =head2 idx returns the index of the bookmark. This will be an integer. =head2 guid returns the guid of the bookmark. This will be a unique value for the hierarchy and 12 characters in length. There are special guids, which are the L, L, L, L and L guids. =head2 keyword returns the L (if any) associated with the bookmark. =head2 last_modified returns the time the bookmark was last modified in seconds since the UNIX epoch. =head2 parent_guid returns the guid of the bookmark's parent. =head2 tags returns the L associated with the bookmark as a list. =head2 title returns the title of the bookmark. This can be for a folder or a bookmark. =head2 TO_JSON required to allow L to work correctly. This method should not need to be called directly. =head2 type returns an integer describing the type of the bookmark. This can be L, L or L. =head2 url returns the URL of the bookmark. It will be returned as a L object. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Bookmark requires no configuration files or environment variables. =head1 DEPENDENCIES Firefox::Marionette::Bookmark requires the following non-core Perl modules =over =item * L =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) 2024, 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.63/lib/Firefox/Marionette/Login.pm0000644000175000017500000002454214763400572021476 0ustar davedavepackage Firefox::Marionette::Login; use strict; use warnings; our $VERSION = '1.63'; 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.63 =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 TO_JSON required to allow L to work correctly. This method should not need to be called directly. =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) 2024, 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.63/lib/Firefox/Marionette/Certificate.pm0000644000175000017500000002405414763400572022646 0ustar davedavepackage Firefox::Marionette::Certificate; use strict; use warnings; our $VERSION = '1.63'; 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 _cert_type { my ($self) = @_; return $self->{certType}; } sub _bitwise_and_with_cert_type { my ( $self, $argument ) = @_; if ( defined $self->_cert_type() ) { return $argument & $self->_cert_type(); } return; } sub is_any_cert { my ($self) = @_; return $self->_bitwise_and_with_cert_type( $self->{ANY_CERT} ); } 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->_bitwise_and_with_cert_type( $self->{UNKNOWN_CERT} ); } 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->_bitwise_and_with_cert_type( $self->{SERVER_CERT} ); } sub is_user_cert { my ($self) = @_; return $self->_bitwise_and_with_cert_type( $self->{USER_CERT} ); } sub subject_name { my ($self) = @_; return $self->{subjectName}; } sub key_usages { my ($self) = @_; return $self->{keyUsages}; } sub is_ca_cert { my ($self) = @_; return $self->_bitwise_and_with_cert_type( $self->{CA_CERT} ); } 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->_bitwise_and_with_cert_type( $self->{EMAIL_CERT} ); } 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.63 =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()) { if ($certificate->is_ca_cert()) { print 'PEM Encoded CA Certificate ' . "\n" . $firefox->certificate_as_pem($certificate) . "\n"; } ... } =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. This looks like a Base64 encoded string approximately 316 bytes long when encoded. =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. This can return false for old browsers that do not support this attribute (such as Firefox 31.1.0esr). =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) 2024, 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.63/lib/Firefox/Marionette/Response.pm0000755000175000017500000001604714763400572022230 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.63'; 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 ( ( ref $error->{message} ) && ( ref $error->{message} eq 'HASH' ) && ( scalar keys %{ $error->{message} } == 0 ) ) { $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.63 =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) 2024, 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.63/lib/Firefox/Marionette/Profile.pm0000755000175000017500000006774114763400572022041 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.63'; 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, $handle ) = @_; if ( defined $handle ) { my $config = Config::INI::Reader->read_handle($handle); return $config; } else { 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 ) = @_; if ( my $profile_directory = $class->directory($name) ) { return File::Spec->catfile( $profile_directory, 'prefs.js' ); } return; } sub _parse_config_for_path { my ( $class, $name, $config, $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 = ( $profile_ini_directory, $config->{$key}->{Path} ); } elsif ( $config->{$key}->{Path} ) { @path = ( $config->{$key}->{Path} ); } else { @path = ( $profile_ini_directory, $config->{$key}->{Default} ); } } } if ( ( !@path ) && ( !defined $name ) && ( defined $first_key ) ) { if ( $config->{$first_key}->{IsRelative} ) { @path = ( $profile_ini_directory, $config->{$first_key}->{Path} ); } else { @path = ( $config->{$first_key}->{Path} ); } } return @path; } sub directory { my ( $class, $name, $config, $profile_ini_directory, $remote_address ) = @_; if ( !$name ) { Firefox::Marionette::Exception->throw( 'No profile name has been supplied'); } $remote_address = $remote_address ? "$remote_address:" : q[]; $profile_ini_directory = $profile_ini_directory ? $profile_ini_directory : $class->profile_ini_directory(); $config = $config ? $config : $class->_read_ini_file($profile_ini_directory); my @path = $class->_parse_config_for_path( $name, $config, $profile_ini_directory ); if ( !@path ) { Firefox::Marionette::Exception->throw( "Failed to find Firefox profile for '$name' in $remote_address$profile_ini_directory" ); } if (wantarray) { return @path; } else { my $path = File::Spec->catfile(@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.toolbars.bookmarks.visibility', 'never', 1 ); $profile->set_value( 'browser.topsites.contile.enabled', '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( 'network.proxy.socks_remote_dns', 'true', 0 ); $profile->set_value( 'security.osclientcerts.autoload', 'true', 0 ); $profile->set_value( 'security.webauth.webauthn_enable_usbtoken', 'false', 0 ); $profile->set_value( 'security.webauth.webauthn_enable_softtoken', 'true', 0 ); $profile->set_value( 'signon.autofillForms', 'false', 0 ); $profile->set_value( 'signon.autologin.proxy', 'true', 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', 'true', 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', 'true', 0 ); $profile->set_value( 'browser.aboutConfig.showWarning', '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.formautofill.addresses.enabled', 'false', 0 ); $profile->set_value( 'extensions.formautofill.creditCards.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.connectivity-service.enabled', 'false', 0 ); $profile->set_value( 'network.cookie.lifetimePolicy', '2', 0 ); $profile->set_value( 'privacy.clearOnShutdown.downloads', 'true', 0 ); $profile->set_value( 'privacy.clearOnShutdown.formdata', 'true', 0 ); $profile->set_value( 'privacy.clearOnShutdown.history', 'true', 0 ); $profile->set_value( 'privacy.clearOnShutdown.offlineApps', 'true', 0 ); $profile->set_value( 'privacy.clearOnShutdown.openWindows', 'true', 0 ); $profile->set_value( 'privacy.clearOnShutdown.sessions', 'true', 0 ); $profile->set_value( 'privacy.clearOnShutdown.siteSettings', 'true', 0 ); $profile->set_value( 'privacy.donottrackheader.enabled', 'true', 0 ); $profile->set_value( 'privacy.fingerprintingProtection', 'true', 0 ); $profile->set_value( 'privacy.fingerprintingProtection.pbmode', 'true', 0 ); $profile->set_value( 'privacy.trackingprotection.enabled', 'true', 0 ); $profile->set_value( 'privacy.trackingprotection.fingerprinting.enabled', 'true', 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( 'signon.management.page.breach-alerts.enabled', '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.63 =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 CONSTANTS =head2 ANY_PORT returns the port number for Firefox to listen on any port (0). =head1 SUBROUTINES/METHODS =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) 2024, 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.63/lib/Firefox/Marionette/Image.pm0000644000175000017500000001202514763400572021441 0ustar davedavepackage Firefox::Marionette::Image; use strict; use warnings; use URI::URL(); use parent qw(Firefox::Marionette::Element); our $VERSION = '1.63'; 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.63 =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 subclass 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) 2024, 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.63/lib/Firefox/Marionette/Window/0000755000175000017500000000000014763402246021330 5ustar davedaveFirefox-Marionette-1.63/lib/Firefox/Marionette/Window/Rect.pm0000755000175000017500000000745214763400572022576 0ustar davedavepackage Firefox::Marionette::Window::Rect; use strict; use warnings; our $VERSION = '1.63'; 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.63 =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) 2024, 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.63/lib/Firefox/Marionette/Cookie.pm0000755000175000017500000001216614763400572021641 0ustar davedavepackage Firefox::Marionette::Cookie; use strict; use warnings; our $VERSION = '1.63'; 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.63 =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) 2024, 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.63/lib/Firefox/Marionette/Buttons.pm0000644000175000017500000000630014763400572022054 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.63'; 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.63 =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 CONSTANTS =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 SUBROUTINES/METHODS None. =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) 2024, 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.63/lib/Firefox/Marionette/Capabilities.pm0000755000175000017500000003157314763400572023024 0ustar davedavepackage Firefox::Marionette::Capabilities; use strict; use warnings; our $VERSION = '1.63'; 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.63 =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. With Firefox 116, this capability is L. 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) 2024, 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.63/lib/Firefox/Marionette/WebWindow.pm0000644000175000017500000000612214763400572022325 0ustar davedavepackage Firefox::Marionette::WebWindow; use strict; use warnings; use parent qw(Firefox::Marionette::LocalObject); our $VERSION = '1.63'; sub IDENTIFIER { return 'window-fcc6-11e5-b4f8-330a88ab9d7f' } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::WebWindow - Represents a Firefox window retrieved using the Marionette protocol =head1 VERSION Version 1.63 =head1 SYNOPSIS use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(); my $original_window = $firefox->window_handle(); my $javascript_window = $firefox->script('return window'); # only works for Firefox 121 and later if ($javascript_window ne $original_window) { die "That was unexpected!!! What happened?"; } =head1 DESCRIPTION This module handles the implementation of a Firefox Window using the Marionette protocol =head1 CONSTANTS =head2 IDENTIFIER returns the L =head1 SUBROUTINES/METHODS =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::WebWindow 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) 2024, 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.63/lib/Firefox/Marionette/Timeouts.pm0000755000175000017500000001013714763400572022235 0ustar davedavepackage Firefox::Marionette::Timeouts; use strict; use warnings; our $VERSION = '1.63'; 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.63 =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) 2024, 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.63/lib/Firefox/Marionette/Exception.pm0000755000175000017500000000626714763400572022373 0ustar davedavepackage Firefox::Marionette::Exception; use strict; use warnings; use Carp(); use overload '""' => 'string'; our $VERSION = '1.63'; 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} . q[ at ] . $self->{origin} . q[ line ] . $self->{line} . 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.63 =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) 2024, 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.63/lib/Firefox/Marionette/Keys.pm0000644000175000017500000002046714763400572021343 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.63'; 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.63 =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 CONSTANTS =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 SUBROUTINES/METHODS None. =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) 2024, 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.63/lib/Firefox/Marionette/Cache.pm0000644000175000017500000003241114763400572021423 0ustar davedavepackage Firefox::Marionette::Cache; use strict; use warnings; use Exporter(); *import = \&Exporter::import; our @EXPORT_OK = qw( CLEAR_COOKIES CLEAR_NETWORK_CACHE CLEAR_IMAGE_CACHE CLEAR_JS_CACHE CLEAR_DOWNLOADS CLEAR_MEDIA_DEVICES CLEAR_DOM_QUOTA CLEAR_PREDICTOR_NETWORK_DATA CLEAR_DOM_PUSH_NOTIFICATIONS CLEAR_HISTORY CLEAR_AUTH_TOKENS CLEAR_AUTH_CACHE CLEAR_PERMISSIONS CLEAR_CONTENT_PREFERENCES CLEAR_HSTS CLEAR_EME CLEAR_REPORTS CLEAR_STORAGE_ACCESS CLEAR_CERT_EXCEPTIONS CLEAR_CONTENT_BLOCKING_RECORDS CLEAR_CSS_CACHE CLEAR_PREFLIGHT_CACHE CLEAR_CLIENT_AUTH_REMEMBER_SERVICE CLEAR_CREDENTIAL_MANAGER_STATE CLEAR_COOKIE_BANNER_EXCEPTION CLEAR_COOKIE_BANNER_EXECUTED_RECORD CLEAR_FINGERPRINTING_PROTECTION_STATE CLEAR_BOUNCE_TRACKING_PROTECTION_STATE CLEAR_MESSAGING_LAYER_SECURITY_STATE CLEAR_STORAGE_PERMISSIONS CLEAR_SHUTDOWN_EXCEPTIONS CLEAR_ALL CLEAR_ALL_CACHES CLEAR_DOM_STORAGES CLEAR_FORGET_ABOUT_SITE CLEAR_COOKIES_AND_SITE_DATA ); our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK, ); our $VERSION = '1.63'; sub CLEAR_COOKIES { return 1 } sub CLEAR_NETWORK_CACHE { return 2 } sub CLEAR_IMAGE_CACHE { return 4 } sub CLEAR_JS_CACHE { return 8 } sub CLEAR_DOWNLOADS { return 16 } sub CLEAR_MEDIA_DEVICES { return 64 } sub CLEAR_DOM_QUOTA { return 128 } sub CLEAR_PREDICTOR_NETWORK_DATA { return 256 } sub CLEAR_DOM_PUSH_NOTIFICATIONS { return 512 } sub CLEAR_HISTORY { return 1024 } sub CLEAR_MESSAGING_LAYER_SECURITY_STATE { return 2048 } sub CLEAR_AUTH_TOKENS { return 4096 } sub CLEAR_AUTH_CACHE { return 8192 } sub CLEAR_SITE_PERMISSIONS { return 16_384 } sub CLEAR_CONTENT_PREFERENCES { return 32_768 } sub CLEAR_HSTS { return 65_536 } sub CLEAR_EME { return 131_072 } sub CLEAR_REPORTS { return 262_144 } sub CLEAR_STORAGE_ACCESS { return 524_288 } sub CLEAR_CERT_EXCEPTIONS { return 1_048_576 } sub CLEAR_CONTENT_BLOCKING_RECORDS { return 2_097_152 } sub CLEAR_CSS_CACHE { return 4_194_304 } sub CLEAR_PREFLIGHT_CACHE { return 8_388_608 } sub CLEAR_CLIENT_AUTH_REMEMBER_SERVICE { return 16_777_216 } sub CLEAR_CREDENTIAL_MANAGER_STATE { return 33_554_432 } sub CLEAR_COOKIE_BANNER_EXCEPTION { return 67_108_864 } sub CLEAR_COOKIE_BANNER_EXECUTED_RECORD { return 134_217_728 } sub CLEAR_FINGERPRINTING_PROTECTION_STATE { return 268_435_456 } sub CLEAR_BOUNCE_TRACKING_PROTECTION_STATE { return 536_870_912 } sub CLEAR_STORAGE_PERMISSIONS { return 1_073_741_824 } sub CLEAR_SHUTDOWN_EXCEPTIONS { return 2_147_483_648 } sub CLEAR_ALL { return 0xFFFFFFFF } sub CLEAR_PERMISSIONS { return CLEAR_SITE_PERMISSIONS() | CLEAR_SHUTDOWN_EXCEPTIONS(); } sub CLEAR_ALL_CACHES { return CLEAR_NETWORK_CACHE() | CLEAR_IMAGE_CACHE() | CLEAR_JS_CACHE() | CLEAR_CSS_CACHE() | CLEAR_PREFLIGHT_CACHE() | CLEAR_HSTS(); } sub CLEAR_DOM_STORAGES { return CLEAR_DOM_QUOTA() | CLEAR_DOM_PUSH_NOTIFICATIONS() | CLEAR_REPORTS(); } sub CLEAR_COOKIES_AND_SITE_DATA { return CLEAR_COOKIES() | CLEAR_COOKIE_BANNER_EXECUTED_RECORD() | CLEAR_DOM_STORAGES() | CLEAR_HSTS() | CLEAR_EME() | CLEAR_AUTH_TOKENS() | CLEAR_AUTH_CACHE() | CLEAR_FINGERPRINTING_PROTECTION_STATE() | CLEAR_BOUNCE_TRACKING_PROTECTION_STATE() | CLEAR_STORAGE_PERMISSIONS() | CLEAR_MESSAGING_LAYER_SECURITY_STATE(); } sub CLEAR_FORGET_ABOUT_SITE { return CLEAR_HISTORY() | CLEAR_ALL_CACHES() | CLEAR_COOKIES() | CLEAR_EME() | CLEAR_DOWNLOADS() | CLEAR_PERMISSIONS() | CLEAR_DOM_STORAGES() | CLEAR_CONTENT_PREFERENCES() | CLEAR_PREDICTOR_NETWORK_DATA() | CLEAR_DOM_PUSH_NOTIFICATIONS() | CLEAR_CLIENT_AUTH_REMEMBER_SERVICE() | CLEAR_REPORTS() | CLEAR_CERT_EXCEPTIONS() | CLEAR_CREDENTIAL_MANAGER_STATE() | CLEAR_COOKIE_BANNER_EXCEPTION() | CLEAR_COOKIE_BANNER_EXECUTED_RECORD() | CLEAR_FINGERPRINTING_PROTECTION_STATE() | CLEAR_BOUNCE_TRACKING_PROTECTION_STATE() | CLEAR_MESSAGING_LAYER_SECURITY_STATE(); } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Cache - Constants to describe actions on the cache =head1 VERSION Version 1.63 =head1 SYNOPSIS use Firefox::Marionette(); use Firefox::Marionette::Cache qw(:all); my $firefox = Firefox::Marionette->new(); $firefox->go('https://google.com'); # where is a good site to produce a lot of cookies? $firefox->go('about:blank'); $firefox->clear_cache(CLEAR_COOKIES()); =head1 DESCRIPTION This module handles the implementation of the Firefox cache constants. This is sourced from L Cache settings in Firefox have changed a lot and are still changing in 2024. The ones that have been historically available AND stable (they have at least retained the same number) have been L, L and L. For the other variables, YMMV. =head1 CONSTANTS =head2 CLEAR_COOKIES returns the value of CLEAR_COOKIES, which is 1 << 0 = 1 =head2 CLEAR_NETWORK_CACHE returns the value of CLEAR_NETWORK_CACHE, which is 1 << 1 = 2 =head2 CLEAR_IMAGE_CACHE returns the value of CLEAR_IMAGE_CACHE, which is 1 << 2 = 4 =head2 CLEAR_JS_CACHE returns the value of CLEAR_JS_CACHE, which is 1 << 3 = 8 =head2 CLEAR_DOWNLOADS returns the value of CLEAR_DOWNLOADS, which is 1 << 4 = 16 =head2 CLEAR_MEDIA_DEVICES returns the value of CLEAR_MEDIA_DEVICES, which is 1 << 6 = 64 =head2 CLEAR_DOM_QUOTA returns the value of CLEAR_DOM_QUOTA, which is 1 << 7 = 128 (LocalStorage, IndexedDB, ServiceWorkers, DOM Cache and so on.) =head2 CLEAR_PREDICTOR_NETWORK_DATA returns the value of CLEAR_PREDICTOR_NETWORK_DATA, which is 1 << 8 = 256 =head2 CLEAR_DOM_PUSH_NOTIFICATIONS returns the value of CLEAR_DOM_PUSH_NOTIFICATIONS, which is 1 << 9 = 512 =head2 CLEAR_HISTORY returns the value of CLEAR_HISTORY, which is 1 << 10 = 1024 (Places history) =head2 CLEAR_MESSAGING_LAYER_SECURITY_STATE returns the value of CLEAR_MESSAGING_LAYER_SECURITY_STATE, which is 1 << 11 = 2048 =head2 CLEAR_AUTH_TOKENS returns the value of CLEAR_AUTH_TOKENS, which is 1 << 12 = 4096 =head2 CLEAR_AUTH_CACHE returns the value of CLEAR_AUTH_CACHE, which is 1 << 13 = 8192 (Login cache) =head2 CLEAR_SITE_PERMISSIONS returns the value of CLEAR_SITE_PERMISSIONS, which is 1 << 14 = 16384 =head2 CLEAR_CONTENT_PREFERENCES returns the value of CLEAR_CONTENT_PREFERENCES, which is 1 << 15 = 32768 =head2 CLEAR_HSTS returns the value of CLEAR_HSTS, which is 1 << 16 = 65536 (HTTP Strict Transport Security data) =head2 CLEAR_EME returns the value of CLEAR_EME, which is 1 << 17 = 131072 (Media plugin data) =head2 CLEAR_REPORTS returns the value of CLEAR_REPORTS, which is 1 << 18 = 262144 (Reporting API reports) =head2 CLEAR_STORAGE_ACCESS returns the value of CLEAR_STORAGE_ACCESS, which is 1 << 19 = 524288 (StorageAccessAPI flag, which indicates user interaction) =head2 CLEAR_CERT_EXCEPTIONS returns the value of CLEAR_CERT_EXCEPTIONS, which is 1 << 20 = 1048576 =head2 CLEAR_CONTENT_BLOCKING_RECORDS returns the value of CLEAR_CONTENT_BLOCKING_RECORDS, which is 1 << 21 = 2097152 (content blocking database) =head2 CLEAR_CSS_CACHE returns the value of CLEAR_CSS_CACHE, which is 1 << 22 = 4194304 (in-memory CSS cache) =head2 CLEAR_PREFLIGHT_CACHE returns the value of CLEAR_PREFLIGHT_CACHE, which is 1 << 23 = 8388608 (CORS preflight cache) =head2 CLEAR_CLIENT_AUTH_REMEMBER_SERVICE returns the value of CLEAR_CLIENT_AUTH_REMEMBER_SERVICE, which is 1 << 24 = 16777216 (clients authentification certificate) =head2 CLEAR_CREDENTIAL_MANAGER_STATE returns the value of CLEAR_CREDENTIAL_MANAGER_STATE, which is 1 << 25 = 33554432 (FedCM) =head2 CLEAR_COOKIE_BANNER_EXCEPTION returns the value of CLEAR_COOKIE_BANNER_EXCEPTION, which is 1 << 26 = 67108864 =head2 CLEAR_COOKIE_BANNER_EXECUTED_RECORD returns the value of CLEAR_COOKIE_BANNER_EXECUTED_RECORD, which is 1 << 27 = 134217728 =head2 CLEAR_FINGERPRINTING_PROTECTION_STATE returns the value of CLEAR_FINGERPRINTING_PROTECTION_STATE, which is 1 << 28 = 268435456 =head2 CLEAR_BOUNCE_TRACKING_PROTECTION_STATE returns the value of CLEAR_BOUNCE_TRACKING_PROTECTION_STATE, which is 1 << 29 = 536870912 =head2 CLEAR_STORAGE_PERMISSIONS returns the value of CLEAR_STORAGE_PERMISSIONS, which is 1 << 30 = 1073741824 =head2 CLEAR_SHUTDOWN_EXCEPTIONS returns the value of CLEAR_SHUTDOWN_EXCEPTIONS, which is 1 << 31 = 2147483648 =head2 CLEAR_ALL returns the value of CLEAR_ALL, which is 4294967295 (0xFFFFFFFF) =head2 CLEAR_PERMISSIONS returns the value of CLEAR_PERMISSIONS, which is (L | L) =head2 CLEAR_ALL_CACHES returns the value of CLEAR_ALL_CACHES, which is (L | L | L | L | L | L) =head2 CLEAR_DOM_STORAGES returns the value of CLEAR_DOM_STORAGES, which is (L | L | L) =head2 CLEAR_FORGET_ABOUT_SITE returns the value of CLEAR_FORGET_ABOUT_SITE, which is (L | L | L | L | L | L | L | L | L | L | L | L | L | L | L | L | L) =head2 CLEAR_COOKIES_AND_SITE_DATA returns the value of CLEAR_COOKIES_AND_SITE_DATA, which is (L | L | L | L | L | L | L | L | L | L | L) =head1 SUBROUTINES/METHODS None. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Cache 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) 2024, 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.63/lib/Firefox/Marionette/Element/0000755000175000017500000000000014763402246021452 5ustar davedaveFirefox-Marionette-1.63/lib/Firefox/Marionette/Element/Rect.pm0000755000175000017500000000677214763400572022724 0ustar davedavepackage Firefox::Marionette::Element::Rect; use strict; use warnings; our $VERSION = '1.63'; 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.63 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $rect = $firefox->find_class('page-content'); say "Current height of the Lucky button " . $rect->height(); say "Height of page-content div is " . $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) 2024, 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.63/lib/Firefox/Marionette/Extension/0000755000175000017500000000000014763402246022035 5ustar davedaveFirefox-Marionette-1.63/lib/Firefox/Marionette/Extension/Timezone.pm0000644000175000017500000002537714763400572024203 0ustar davedavepackage Firefox::Marionette::Extension::Timezone; use File::HomeDir(); use File::Spec(); use Archive::Zip(); use English qw( -no_match_vars ); use strict; use warnings; our $VERSION = '1.63'; my $content_name = 'content.js'; sub new { my ( $class, %timezone_parameters ) = @_; my $zip = Archive::Zip->new(); my $manifest = $zip->addString( $class->_manifest_contents(), 'manifest.json' ); $manifest->desiredCompressionMethod( Archive::Zip::COMPRESSION_DEFLATED() ); my $content = $zip->addString( $class->_content_contents(%timezone_parameters), $content_name ); $content->desiredCompressionMethod( Archive::Zip::COMPRESSION_DEFLATED() ); return $zip; } sub _manifest_contents { my ($class) = @_; return <<"_JS_"; { "description": "Firefox::Marionette Timezone extension", "manifest_version": 2, "name": "Firefox Marionette Timezone extension", "version": "1.1", "permissions": [ "activeTab" ], "content_scripts": [ { "matches": [""], "js": ["$content_name"], "match_about_blank": true, "run_at": "document_start", "all_frames": true } ] } _JS_ } sub _content_contents { my ( $class, %timezone_parameters ) = @_; my $timezone_contents = $class->timezone_contents(%timezone_parameters); $timezone_contents =~ s/\s+/ /smxg; $timezone_contents =~ s/\\n/\\\\n/smxg; return <<"_JS_"; { let script = document.createElement('script'); let text = document.createTextNode('$timezone_contents'); script.appendChild(text); (document.head || document.documentElement).appendChild(script); script.remove(); } _JS_ } sub timezone_contents { my ( $class, %parameters ) = @_; my $encoded_timezone = URI::Escape::uri_escape( $parameters{timezone} ); my $encoded_locale = URI::Escape::uri_escape('en-US'); my $contents = <<"_JS_"; { if (("console" in window) && ("log" in window.console)) { console.log("Loading Firefox::Marionette::Extension::Timezone"); } let tz = decodeURIComponent("$encoded_timezone"); let locale = decodeURIComponent("$encoded_locale"); let setTimezone = function(win) { win.Date.prototype.toString = function() { return win.Intl.DateTimeFormat(locale, { weekday: "short", timeZone: tz }).format(this.valueOf()) + " " + win.Intl.DateTimeFormat(locale, { month: "short", timeZone: tz }).format(this.valueOf()) + " " + win.Intl.DateTimeFormat(locale, { day: "2-digit", timeZone: tz }).format(this.valueOf()) + " " + win.Intl.DateTimeFormat(locale, { year: "numeric", timeZone: tz }).format(this.valueOf()) + " " + win.Intl.DateTimeFormat(locale, { hour: "2-digit", hourCycle: "h24", timeZone: tz }).format(this.valueOf()) + ":" + win.Intl.DateTimeFormat(locale, { minute: "2-digit", timeZone: tz }).format(this.valueOf()) + ":" + win.Intl.DateTimeFormat(locale, { second: "2-digit", timeZone: tz }).format(this.valueOf()) + " " + win.Intl.DateTimeFormat(locale, { timeZoneName: "longOffset", timeZone: tz }).format(this.valueOf()).replace(/:/,"").replace(/^[0-9]+.[0-9]+.[0-9]+,?[ ]/, "") + " (" + win.Intl.DateTimeFormat(locale, { timeZoneName: "long", timeZone: tz }).format(this.valueOf()).replace(/^[0-9]+.[0-9]+.[0-9]+,?[ ]/, "") + ")" }; /* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString#options */ win.Date.prototype.toLocaleString = function() { return win.Intl.DateTimeFormat(locale, { year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric", timeZone: tz }).format(this.valueOf()) }; /* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#date-time_component_options */ win.Date.prototype.getDate = function() { return win.Intl.DateTimeFormat(locale, { day: "numeric", timeZone: tz }).format(this.valueOf()) }; win.Date.prototype.getDay = function() { switch (win.Intl.DateTimeFormat("en-US", { weekday: "short", timeZone: tz }).format(this.valueOf())) { case "Sun": return 0; case "Mon": return 1; case "Tue": return 2; case "Wed": return 3; case "Thu": return 4; case "Fri": return 5; case "Sat": return 6 } }; win.Date.prototype.getMonth = function() { return parseInt(win.Intl.DateTimeFormat(locale, { month: "numeric", timeZone: tz }).format(this.valueOf()), 10) - 1 }; win.Date.prototype.getFullYear = function() { return win.Intl.DateTimeFormat(locale, { year: "numeric", timeZone: tz }).format(this.valueOf()) }; win.Date.prototype.getHours = function() { return parseInt(win.Intl.DateTimeFormat(locale, { hour: "numeric", hourCycle: "h24", timeZone: tz }).format(this.valueOf()), 10) }; win.Date.prototype.getMinutes = function() { return parseInt(win.Intl.DateTimeFormat(locale, { minute: "numeric", timeZone: tz }).format(this.valueOf()), 10) }; let idtc = win.Intl.Collator; win.Intl.Collator = function() { if (!arguments[0]) { arguments[0] = [ locale ] } return new idtc(arguments[0]) }; let idtf = win.Intl.DateTimeFormat; win.Intl.DateTimeFormat = function() { if (arguments[1]) { if (!arguments[1]["timeZone"]) { arguments[1]["timeZone"] = tz } } else { arguments[1] = { "timeZone": tz } } if (!arguments[0]) { arguments[0] = locale; } return idtf(arguments[0], arguments[1]); }; let idtn = win.Intl.DisplayNames; win.Intl.DisplayNames = function() { if (!arguments[0]) { arguments[0] = [ locale ] } return new idtn(arguments[0], arguments[1]) }; if ("DurationFormat" in win.Intl) { let idf = win.Intl.DurationFormat; win.Intl.DurationFormat = function() { if (!arguments[0]) { arguments[0] = [ locale ] } return idf(arguments[0], arguments[1]) }; } let ilf = win.Intl.ListFormat; win.Intl.ListFormat = function() { if (!arguments[0]) { arguments[0] = [ locale ] } return new ilf(arguments[0], arguments[1]) }; let idtl = win.Intl.Locale; win.Intl.Locale = function() { if (!arguments[0]) { arguments[0] = [ locale ] } return new idtl(arguments[0]) }; let inf = win.Intl.NumberFormat; win.Intl.NumberFormat = function() { if (!arguments[0]) { arguments[0] = [ locale ] } return new inf(arguments[0], arguments[1]) }; let ipr = win.Intl.PluralRules; win.Intl.PluralRules = function() { if (!arguments[0]) { arguments[0] = [ locale ] } return new ipr(arguments[0]) }; let irtf = win.Intl.RelativeTimeFormat; win.Intl.RelativeTimeFormat = function() { if (!arguments[0]) { arguments[0] = [ locale ] } return new irtf(arguments[0], arguments[1]) }; let isg = win.Intl.Segmenter; win.Intl.Segmenter = function() { if (!arguments[0]) { arguments[0] = [ locale ] } return new isg(arguments[0], arguments[1]) }; }; setTimezone(window); /* https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver */ new MutationObserver((mutationList, observer) => { for (let mutation of mutationList) { if (mutation.type === "childList") { for (let node of mutation.addedNodes) { if (node.nodeName === "IFRAME") { if (node.contentWindow !== null) { setTimezone(node.contentWindow); } } } } } }).observe((document.head || document.documentElement), { attributes: true, childList: true, subtree: true }); if ("Worker" in window) { let tzw = window.Worker; window.Worker = function(url) { console.log("Worker told to start with " + url); return new tzw(url); }; } if (("console" in window) && ("log" in window.console)) { console.log("Loaded Firefox::Marionette::Extension::Timezone"); } } _JS_ return $contents; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Extension::Timezone - Contains the Timezone Extension =head1 VERSION Version 1.63 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new(stealth => 1); $firefox->go("https://fastapi.metacpan.org/v1/download_url/Firefox::Marionette"); =head1 DESCRIPTION This module contains the Timezone extension. This module should not be used directly. It is required when the 'stealth' parameter is supplied to the L method in L. =head1 SUBROUTINES/METHODS =head2 new Returns a L of the Timezone extension. =head2 timezone_contents Returns the javascript used to setup a different (or the original) user agent as a string. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Extension::Timezone 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) 2024, 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.63/lib/Firefox/Marionette/Extension/Stealth.pm0000644000175000017500000010651014763400572024002 0ustar davedavepackage Firefox::Marionette::Extension::Stealth; use File::HomeDir(); use File::Spec(); use Archive::Zip(); use English qw( -no_match_vars ); use strict; use warnings; our $VERSION = '1.63'; sub _BUFFER_SIZE { return 65_536 } sub _MSIE_VERSION { return 11 } my $content_name = 'content.js'; my %_function_bodies = ( 'Navigator.bluetooth' => q[return { getAvailability: function () { return new Promise((resolve, reject) => resolve(false))} };], 'Navigator.canShare' => q[return true], 'Navigator.clearAppBadge' => q[return new Promise((resolve, reject) => resolve(undefined))], 'Navigator.connection' => q[let downlink = (new Array(7.55, 1.6))[Math.floor(Math.random() * 2)]; let rtt = (new Array(50, 100))[Math.floor(Math.random() * 2)]; let obj = { onchange: null, effectiveType: decodeURIComponent(\\x27%274g%27\\x27), rtt: rtt, downlink: downlink, saveData: false }; return new NetworkInformation(obj)], 'Navigator.deprecatedReplaceInURN' => q[throw TypeError(decodeURIComponent(\\x27Failed to execute %27deprecatedReplaceInURN%27 on %27Navigator%27: Passed URL must be a valid URN URL.\\x27))], 'Navigator.deprecatedURNtoURL' => q[throw TypeError(decodeURIComponent(\\x27Failed to execute %27deprecatedURNtoURL%27 on %27Navigator%27: Passed URL must be a valid URN URL.\\x27))], 'Navigator.deviceMemory' => q[return (navigator.hardwareConcurrency < 4 ? 4 : 8)], 'Navigator.getBattery' => q[return new Promise((resolve, reject) => resolve({ charging: true, chargingTime: 0, dischargingTime: Infinity, level: 1, onchargingchange: null }))], 'Navigator.getGamePads' => q[return new Array( null, null, null, null )], 'Navigator.getInstalledRelatedApps' => q[return new Promise((resolve,reject) => resolve([]))], 'Navigator.getUserMedia' => q[return getUserMedia], 'Navigator.gpu' => q[return { wgslLanguageFeatures: { size: 0 } }], 'Navigator.hid' => q[return { getDevices: function() { return new Promise((resolve,reject) => resolve([])) }, requestDevices: function() { return new Promise((resolve, reject) => resolve([])) } }], 'Navigator.ink' => q[return {}], 'Navigator.keyboard' => q[return { getLayoutMap: function() { }, lock: function() { }, unlock: function() { } }], 'Navigator.locks' => q[return { query: function() { }, request: function() { } }], 'Navigator.login' => q[return { setStatus: function() { return undefined } }], 'Navigator.webkitGetUserMedia' => q[return getUserMedia], 'Navigator.xr' => q[return new XRSystem({ondevicechange: null})], ); sub new { my ( $class, %agent_parameters ) = @_; my $zip = Archive::Zip->new(); my $manifest = $zip->addString( $class->_manifest_contents(), 'manifest.json' ); $manifest->desiredCompressionMethod( Archive::Zip::COMPRESSION_DEFLATED() ); my $content = $zip->addString( $class->_content_contents(%agent_parameters), $content_name ); $content->desiredCompressionMethod( Archive::Zip::COMPRESSION_DEFLATED() ); return $zip; } sub _manifest_contents { my ($class) = @_; return <<"_JS_"; { "description": "Firefox::Marionette Stealth extension", "manifest_version": 2, "name": "Firefox Marionette Stealth extension", "version": "1.1", "permissions": [ "activeTab" ], "content_scripts": [ { "matches": [""], "js": ["$content_name"], "match_about_blank": true, "run_at": "document_start", "all_frames": true } ] } _JS_ } sub _content_contents { my ( $class, %agent_parameters ) = @_; my $user_agent_contents = $class->user_agent_contents(%agent_parameters); $user_agent_contents =~ s/\s+/ /smxg; $user_agent_contents =~ s/\\n/\\\\n/smxg; return <<"_JS_"; { let script = document.createElement('script'); let text = document.createTextNode('$user_agent_contents'); script.appendChild(text); (document.head || document.documentElement).appendChild(script); script.remove(); } _JS_ } my $_function_definition_count = 1; sub _get_browser_type_and_version { my ( $class, $user_agent_string ) = @_; my ( $browser_type, $browser_version ); if ( $user_agent_string =~ /Chrome\/(\d+)/smx ) { ( $browser_type, $browser_version ) = ( 'chrome', $1 ); if ( $user_agent_string =~ /Edg(?:[eA]|iOS)?\/(\d+)/smx ) { ( $browser_type, $browser_version ) = ( 'edge', $1 ); } elsif ( $user_agent_string =~ /(?:Opera|Presto|OPR)\/(\d+)/smx ) { ( $browser_type, $browser_version ) = ( 'opera', $1 ); } } elsif ( $user_agent_string =~ /Version\/(\d+)(?:[.]\d+)?[ ].*Safari\/\d+/smx ) { ( $browser_type, $browser_version ) = ( 'safari', $1 ); } elsif ( $user_agent_string =~ /Trident/smx ) { ( $browser_type, $browser_version ) = ( 'ie', _MSIE_VERSION() ); } my $general_token_re = qr/Mozilla\/5[.]0[ ]/smx; my $platform_etc_re = qr/[(][^)]+[)][ ]/smx; my $gecko_trail_re = qr/Gecko\/20100101[ ]/smx; my $firefox_version_re = qr/Firefox\/(\d+)[.]0/smx; if ( $user_agent_string =~ /^$general_token_re$platform_etc_re$gecko_trail_re$firefox_version_re$/smx ) { ( $browser_type, $browser_version ) = ( 'firefox', $1 ); } return ( $browser_type, $browser_version ); } sub user_agent_contents { my ( $class, %parameters ) = @_; my ( $to_browser_type, $to_browser_version ); if ( defined $parameters{to} ) { ( $to_browser_type, $to_browser_version ) = $class->_get_browser_type_and_version( $parameters{to} ); } $_function_definition_count = 1; my ( $definition_name, $function_definition ) = $class->_get_js_function_definition( $to_browser_type, 'Navigator', 'webdriver', 'return false' ); my $native_code_body = $class->_native_code_body($to_browser_type); my $contents = <<"_JS_"; { if (("console" in window) && ("log" in window.console)) { console.log("Loading Firefox::Marionette::Extension::Stealth"); } let navProto = Object.getPrototypeOf(window.navigator); let winProto = Object.getPrototypeOf(window); $function_definition Object.defineProperty(navProto, "webdriver", {get: $definition_name, enumerable: false, configurable: true}); console.clear = function() { console.log("$class blocked an attempt at clearing the console...") }; console.clear.toString = function clear_def() { return "function clear() $native_code_body" }; let getUserMedia = navProto.mozGetUserMedia; _JS_ my ( $from_browser_type, $from_browser_version ); if ( defined $to_browser_type ) { ( $from_browser_type, $from_browser_version ) = $class->_get_browser_type_and_version( $parameters{from} ); if ( ( defined $from_browser_type ) && ( $from_browser_type ne $to_browser_type ) ) { $contents .= <<"_JS_"; window.eval.toString = function eval_def() { return "function eval() $native_code_body" }; Function.prototype.bind.toString = function bind_def() { return "function bind() $native_code_body" }; _JS_ } if ( ( $to_browser_type eq 'chrome' ) || ( $to_browser_type eq 'edge' ) || ( $to_browser_type eq 'opera' ) ) { $contents .= $class->_check_and_add_function( $to_browser_type, 'Navigator.webkitPersistentStorage', { function_body => q[return { queryUsageAndQuota: function() { return undefined }, requestQuota: function() { return undefined } }] }, {} ); $contents .= $class->_check_and_add_function( $to_browser_type, 'Navigator.webkitTemporaryStorage', { function_body => q[return { queryUsageAndQuota: function() { return undefined }, requestQuota: function() { return undefined } }] }, {} ); $contents .= $class->_check_and_add_function( $to_browser_type, 'Window.webkitResolveLocalFileSystemURL', { function_body => q[return undefined] }, {} ); $contents .= $class->_check_and_add_function( $to_browser_type, 'Window.webkitMediaStream', { function_body => q[return undefined] }, {} ); $contents .= $class->_check_and_add_function( $to_browser_type, 'Window.webkitSpeechGrammar', { function_body => q[return undefined] }, {} ); $contents .= <<'_JS_'; delete navProto.oscpu; delete window.ApplePayError; delete window.CSSPrimitiveValue; delete window.Counter; delete navigator.getStorageUpdates; delete window.WebKitMediaKeys; let chrome = { csi: function () { }, getVariableName: function () { }, loadTimes: function () { }, metricsPrivate: { MetricTypeType: { HISTOGRAM_LINEAR: "histogram-linear", HISTOGRAM_LOG: "histogram-log" }, getFieldTrial: function () { }, getHistogram: function () { }, getVariationParams: function () { }, recordBoolean: function () { }, recordCount: function () { }, recordEnumerationValue: function () { }, recordLongTime: function () { }, recordMediumCount: function () { }, recordMediumTime: function () { }, recordPercentage: function () { }, recordSmallCount: function () { }, recordSparseValue: function () { }, recordSparseValueWithHashMetricName: function () { }, recordSparseValueWithPersistentHash: function () { }, recordTime: function () { }, recordUserAction: function () { }, recordValue: function () { } }, send: function () { }, timeTicks: { nowInMicroseconds: function () { } }, webstore: function () { }, app: function () { }, runtime: { connect: function() { }, sendMessage: function() { } } }; Object.defineProperty(winProto, "chrome", {value: chrome, writable: true, enumerable: true, configurable: true}); let canLoadAdAuctionFencedFrame = function() { return true }; Object.defineProperty(navProto, "canLoadAdAuctionFencedFrame", {value: canLoadAdAuctionFencedFrame, writable: true, enumerable: true, configurable: true}); let createAuctionNonce = function() { return crypto.randomUUID() }; Object.defineProperty(navProto, "createAuctionNonce", {value: createAuctionNonce, writable: true, enumerable: true, configurable: true}); Object.defineProperty(navProto, "deprecatedRunAdAuctionEnforcesKAnonymity", {value: false, writable: true, enumerable: true, configurable: true}); _JS_ if ( $to_browser_type eq 'edge' ) { } elsif ( $to_browser_type eq 'opera' ) { my ( $scrap_name, $scrap_definition ) = $class->_get_js_function_definition( $to_browser_type, 'Window', 'scrap', 'return null' ); $contents .= <<"_JS_"; $scrap_definition Object.defineProperty(winProto, "g_opr", {value: {scrap: $scrap_name}, enumerable: true, configurable: true}); Object.defineProperty(winProto, "opr", {value: {}, enumerable: true, configurable: true}); _JS_ } } elsif ( $to_browser_type eq 'safari' ) { $contents .= <<'_JS_'; delete navProto.oscpu; delete navigator.webkitPersistentStorage; delete navigator.webkitTemporaryStorage; delete window.webkitResolveLocalFileSystemURL; delete window.webkitMediaStream; delete window.webkitSpeechGrammar; _JS_ $contents .= $class->_check_and_add_function( $to_browser_type, 'Window.ApplePayError', { function_body => q[return undefined] }, {} ); $contents .= $class->_check_and_add_function( $to_browser_type, 'Window.CSSPrimitiveValue', { function_body => q[return undefined] }, {} ); $contents .= $class->_check_and_add_function( $to_browser_type, 'Window.Counter', { function_body => q[return undefined] }, {} ); $contents .= $class->_check_and_add_function( $to_browser_type, 'Navigator.getStorageUpdates', { function_body => q[return undefined] }, {} ); $contents .= $class->_check_and_add_function( $to_browser_type, 'Window.WebKitMediaKeys', { function_body => q[return undefined] }, {} ); } elsif ( $to_browser_type eq 'ie' ) { $contents .= <<'_JS_'; let docProto = Object.getPrototypeOf(window.document); delete navigator.webkitPersistentStorage; delete navigator.webkitTemporaryStorage; delete window.webkitResolveLocalFileSystemURL; delete window.webkitMediaStream; delete window.webkitSpeechGrammar; delete window.ApplePayError; delete window.CSSPrimitiveValue; delete window.Counter; delete navigator.getStorageUpdates; delete window.WebKitMediaKeys; Object.defineProperty(docProto, "documentMode", {value: true, writable: true, enumerable: true, configurable: true}); Object.defineProperty(navProto, "msDoNotTrack", {value: "0", writable: true, configurable: true}); Object.defineProperty(winProto, "msWriteProfilerMark", {value: {}, writable: true, configurable: true}); _JS_ } if ( $to_browser_type eq 'firefox' ) { $contents .= <<'_JS_'; delete navigator.webkitPersistentStorage; delete navigator.webkitTemporaryStorage; delete window.webkitResolveLocalFileSystemURL; delete window.webkitMediaStream; delete window.webkitSpeechGrammar; delete window.ApplePayError; delete window.CSSPrimitiveValue; delete window.Counter; delete navigator.getStorageUpdates; delete window.WebKitMediaKeys; if ("onmozfullscreenchange" in window) { } else { Object.defineProperty(window, "onmozfullscreenchange", {value: undefined, writable: true, configurable: true}); } if ("mozInnerScreenX" in window) { } else { Object.defineProperty(window, "mozInnerScreenX", {value: 0, writable: true, configurable: true}); } if ("CSSMozDocumentRule" in window) { } else { Object.defineProperty(window, "CSSMozDocumentRule", {value: undefined, writable: true, configurable: true}); } if ("CanvasCaptureMediaStream" in window) { } else { Object.defineProperty(window, "CanvasCaptureMediaStream", {value: undefined, writable: true, configurable: true}); } _JS_ } else { $contents .= <<'_JS_'; delete navProto.buildID; delete window.InstallTrigger; delete navProto.mozGetUserMedia; delete window.onmozfullscreenchange; delete window.mozInnerScreenX; delete window.CSSMozDocumentRule; delete window.CanvasCaptureMediaStream; _JS_ } if ($from_browser_version) { $contents .= $class->_browser_compat_data( from_browser_type => $from_browser_type, from_browser_version => $from_browser_version, to_browser_type => $to_browser_type, to_browser_version => $to_browser_version, filters => $parameters{filters}, ); } } my %navigator_agent_mappings = ( to => 'userAgent', app_version => 'appVersion', platform => 'platform', product => 'product', product_sub => 'productSub', vendor => 'vendor', vendor_sub => 'vendorSub', oscpu => 'oscpu', ); foreach my $key ( sort { $a cmp $b } keys %navigator_agent_mappings ) { if ( defined $parameters{$key} ) { my $encoded = URI::Escape::uri_escape( $parameters{$key} ); $contents .= $class->_check_and_add_function( $to_browser_type, 'Navigator.' . $navigator_agent_mappings{$key}, { function_body => qq[return decodeURIComponent(\\x27$encoded\\x27)], override => 1 }, {} ); } elsif ( $parameters{to} ) { $contents .= <<"_JS_"; delete navigator.$navigator_agent_mappings{$key}; _JS_ } } $contents .= <<'_JS_'; if (("console" in window) && ("log" in window.console)) { console.log("Loaded Firefox::Marionette::Extension::Stealth"); } } _JS_ return $contents; } sub _check_and_add_class { my ( $class, $property_name, $to_string_tag_allowed ) = @_; my $javascript_class = $property_name; my $win_proto_class = $javascript_class; $win_proto_class =~ s/^Window[.]/winProto./smx; my $contents = <<"_JS_"; if ("$javascript_class" in window) { } else { window.$javascript_class = class { constructor(obj) { for(let key of Object.keys(obj)) { Object.defineProperty(this, key, {value: obj[key], enumerable: true, configurable: true}); } } _JS_ if ($to_string_tag_allowed) { $contents .= <<"_JS_"; get [Symbol.toStringTag]() { return "$javascript_class"; } _JS_ } $contents .= <<"_JS_"; }; } _JS_ return $contents; } sub _native_code_body { my ( $class, $to_browser_type ) = @_; my $native_code_body = q[{] . q[\\] . 'n\\x20\\x20\\x20\\x20[native code]' . q[\\] . q[n}]; if ( ( defined $to_browser_type ) && ( $to_browser_type eq 'chrome' ) ) { $native_code_body = q[{ [native code] }]; } return $native_code_body; } sub _get_js_function_definition { my ( $class, $to_browser_type, $javascript_class, $name, $function_body ) = @_; $function_body = <<"_JS_"; if ($javascript_class.prototype.isPrototypeOf(this)) { $function_body } else { throw TypeError(decodeURIComponent(\\x27%27get $name%27 called on an object that does not implement interface $javascript_class.\\x27)); } _JS_ $_function_definition_count += 1; my $native_code_body = $class->_native_code_body($to_browser_type); my $actual_name = "fm_def_$_function_definition_count"; return ( $actual_name, <<"_JS_"); let $actual_name = new Function("$function_body"); $actual_name.toString = function fm_def() { return "function ${name}() $native_code_body" }; _JS_ } sub _check_and_add_function { my ( $class, $to_browser_type, $property_name, $proposed_change_properties, $deleted_classes ) = @_; my $javascript_class = $property_name; $javascript_class =~ s/[.][\-_@[:alnum:]]+$//smx; my $function_name = $property_name; $function_name =~ s/^.*[.]([\-_@[:alnum:]]+)$/$1/smx; my $parent_class = $javascript_class; if ( $javascript_class =~ /^(.*?)[.]/smx ) { $parent_class = ($1); } my $contents = q[]; if ( !$deleted_classes->{$javascript_class} ) { my ( $definition_name, $function_definition ) = $class->_get_js_function_definition( $to_browser_type, $javascript_class, $function_name, $proposed_change_properties->{function_body} ); $contents .= <<"_JS_"; $function_definition _JS_ if ( $proposed_change_properties->{override} ) { $contents .= <<"_JS_"; # So far, all overrides are Navigator overrides Object.defineProperty(window.$javascript_class, "$function_name", {get: $definition_name, enumerable: true, configurable: true}); Object.defineProperty(navProto, "$function_name", {get: $definition_name, enumerable: true, configurable: true}); _JS_ } elsif ( $javascript_class eq 'Window' ) { $contents .= <<"_JS_"; if ("$function_name" in window) { } else { Object.defineProperty(window, "$function_name", {get: $definition_name, enumerable: true, configurable: true}); } _JS_ } else { $contents .= <<"_JS_"; if (winProto.$parent_class && winProto.$javascript_class) { if ("$function_name" in winProto.$javascript_class) { } else { Object.defineProperty(winProto.$javascript_class.prototype, "$function_name", {get: $definition_name, enumerable: true, configurable: true}); } } else if (window.$parent_class && window.$javascript_class) { if (window.$javascript_class.prototype) { if ("$function_name" in window.$javascript_class.prototype) { } else { Object.defineProperty(window.$javascript_class.prototype, "$function_name", {get: $definition_name, enumerable: true, configurable: true}); } } else { if ("$function_name" in window.$javascript_class) { } else { Object.defineProperty(window.$javascript_class, "$function_name", {get: $definition_name, enumerable: true, configurable: true}); } } _JS_ if ( $javascript_class eq 'Navigator' ) { $contents .= <<"_JS_"; Object.defineProperty(navProto, "$function_name", {get: $definition_name, enumerable: true, configurable: true}); _JS_ } elsif ( $javascript_class eq 'Document' ) { $contents .= <<"_JS_"; Object.defineProperty(window.document, "$function_name", {get: $definition_name, enumerable: true, configurable: true}); _JS_ } $contents .= <<"_JS_"; } _JS_ } } return $contents; } sub _check_and_delete_class { my ( $class, $property_name ) = @_; my $parent_class = $property_name; if ( $property_name =~ /^(.*?)[.]/smx ) { ($parent_class) = ($1); } my $contents = <<"_JS_"; if (winProto.$parent_class && winProto.$property_name) { delete winProto.$property_name; } else if (window.$parent_class && window.$property_name) { delete window.$property_name; } _JS_ if ( $property_name eq 'SubtleCrypto' ) { $contents .= <<'_JS_'; if ("crypto" in window) { delete window["crypto"]; } _JS_ } else { my $lc_property_name = lc $property_name; $contents .= <<"_JS_"; if ("$lc_property_name" in window) { delete window["$lc_property_name"]; } _JS_ } return $contents; } sub _check_and_delete_function { my ( $class, $property_name, $deleted_classes ) = @_; my $contents = q[]; my $javascript_class = $property_name; $javascript_class =~ s/[.][\-_@[:alpha:]]+$//smx; my $function_name = $property_name; $function_name =~ s/^.*[.]([\-_@[:alpha:]]+)$/$1/smx; my $parent_class = $javascript_class; if ( $javascript_class =~ /^(.*?)[.]/smx ) { $parent_class = ($1); } if ( !$deleted_classes->{$javascript_class} ) { $contents .= <<"_JS_"; if (winProto.$parent_class && winProto.$javascript_class) { delete winProto.$javascript_class\["$function_name"\]; } else if (window.$parent_class && window.$javascript_class) { if (window.$javascript_class.prototype) { delete window.$javascript_class.prototype\["$function_name"\]; } delete window.$javascript_class\["$function_name"\]; } _JS_ } if ( $javascript_class eq 'Navigator' ) { $contents .= <<"_JS_"; if ("$function_name" in navProto) { delete navProto["$function_name"]; } else if ("$function_name" in navigator) { delete navigator["$function_name"]; } _JS_ } elsif ( $javascript_class eq 'Document' ) { $contents .= <<"_JS_"; if (("document" in winProto) && ("$function_name" in winProto.document)) { delete winProto.document["$function_name"]; } else if (("document" in window) && ("$function_name" in window.document)) { delete window.document["$function_name"]; } _JS_ } elsif ( $javascript_class eq 'Window' ) { $contents .= <<"_JS_"; if ("$function_name" in winProto) { delete winProto["$function_name"]; } else if ("$function_name" in window) { delete window["$function_name"]; } _JS_ } return $contents; } sub _read_bcd { my ($class) = @_; my %browser_properties; my $bcd_path = Firefox::Marionette::BCD_PATH(); if ( ( defined $bcd_path ) && ( my $bcd_handle = FileHandle->new( $bcd_path, Fcntl::O_RDONLY() ) ) ) { my $bcd_contents; my $result; while ( $result = $bcd_handle->read( my $buffer, _BUFFER_SIZE() ) ) { $bcd_contents .= $buffer; } close $bcd_handle or Firefox::Marionette::Exception->throw( "Failed to close '$bcd_path':$EXTENDED_OS_ERROR"); %browser_properties = %{ JSON->new()->decode($bcd_contents) }; } elsif ( $OS_ERROR == POSIX::ENOENT() ) { Carp::carp( q[BCD file is not available. Please run 'build-bcd-for-firefox']); } else { Firefox::Marionette::Exception->throw( "Failed to open '$bcd_path' for reading:$EXTENDED_OS_ERROR"); } return %browser_properties; } sub _available_in { my ( $class, %properties ) = @_; my $available; my $browser_type = $properties{browser_type}; foreach my $proposed_change_properties ( @{ $properties{changes} } ) { if ( $proposed_change_properties->{add} ) { if ( $proposed_change_properties->{add} <= $properties{browser_version} ) { if ( !$proposed_change_properties->{pref_name} ) { if ( !defined $proposed_change_properties->{function_body} ) { $proposed_change_properties->{function_body} = $_function_bodies{ $properties{property_name} } || 'return null'; } $available = $proposed_change_properties; } } } elsif ( defined $proposed_change_properties->{rm} ) { if ( $proposed_change_properties->{rm} <= $properties{browser_version} ) { $available = undef; } } } return $available; } sub _this_change_should_be_processed { my ( $class, $proposed_change, $property_name, $change_number, $filters ) = @_; if ( defined $filters ) { if ( $property_name !~ /$filters/smx ) { return 0; } } return 1; } sub _browser_compat_data { my ( $class, %parameters ) = @_; my %browser_properties = $class->_read_bcd(); my $contents = q[]; my %deleted_classes; my $change_number = 0; VERSION: foreach my $property_name ( sort { $a cmp $b } keys %browser_properties ) { my $property_object = $browser_properties{$property_name}; my $property_type = $property_object->{type}; my %from_properties = ( browser_type => $parameters{from_browser_type}, browser_version => $parameters{from_browser_version}, property_type => $property_type, property_name => $property_name, changes => $browser_properties{$property_name}{browsers} { $parameters{from_browser_type} }, ); my %to_properties = ( browser_type => $parameters{to_browser_type}, browser_version => $parameters{to_browser_version}, property_type => $property_type, property_name => $property_name, changes => $browser_properties{$property_name}{browsers} { $parameters{to_browser_type} }, ); my ( $delete_property, $add_property, $change_properties ); if ( my $proposed_change_properties = $class->_available_in(%from_properties) ) { if ( !$class->_available_in(%to_properties) ) { $delete_property = 1; } } else { if ( my $proposed_change_properties = $class->_available_in(%to_properties) ) { $add_property = 1; $change_properties = $proposed_change_properties; } } my $to_string_tag_allowed = 0; if ( defined $browser_properties{'Symbol.toStringTag'}{browsers} { $parameters{to_browser_type} }[0]{add} ) { if ( $browser_properties{'Symbol.toStringTag'}{browsers} { $parameters{to_browser_type} }[0]{add} < $parameters{to_browser_version} ) { $to_string_tag_allowed = 1; } } my $change_details = { to_browser_type => $parameters{to_browser_type}, delete_property => $delete_property, add_property => $add_property, change_number => $change_number, property_name => $property_name, property_type => $property_type, filters => $parameters{filters}, proposed_change_properties => $change_properties, deleted_classes => \%deleted_classes, to_string_tag_allowed => $to_string_tag_allowed, }; $contents .= $class->_process_change($change_details); } return $contents; } sub _process_change { my ( $class, $change_details ) = @_; my $contents = q[]; if ( $change_details->{delete_property} ) { if ( $change_details->{property_type} eq 'class' ) { my $proposed_change = $class->_check_and_delete_class( $change_details->{property_name} ); if ( $class->_this_change_should_be_processed( $proposed_change, $change_details->{property_name}, $change_details->{change_number}, $change_details->{filters}, ) ) { $contents .= $proposed_change; $change_details->{deleted_classes} ->{ $change_details->{property_name} } = 1; } } else { if ( my $proposed_change = $class->_check_and_delete_function( $change_details->{property_name}, $change_details->{deleted_classes} ) ) { if ( $class->_this_change_should_be_processed( $proposed_change, $change_details->{property_name}, $change_details->{change_number}, $change_details->{filters}, ) ) { $contents .= $proposed_change; } } } } elsif ( $change_details->{add_property} ) { if ( $change_details->{property_type} eq 'class' ) { my $proposed_change = $class->_check_and_add_class( $change_details->{property_name}, $change_details->{to_string_tag_allowed} ); if ( $class->_this_change_should_be_processed( $proposed_change, $change_details->{property_name}, $change_details->{change_number}, $change_details->{filters}, ) ) { $contents .= $proposed_change; } } else { if ( my $proposed_change = $class->_check_and_add_function( $change_details->{to_browser_type}, $change_details->{property_name}, $change_details->{proposed_change_properties}, $change_details->{deleted_classes} ) ) { if ( $class->_this_change_should_be_processed( $proposed_change, $change_details->{property_name}, $change_details->{change_number}, $change_details->{filters}, ) ) { $contents .= $proposed_change; } } } } return $contents; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Extension::Stealth - Contains the Stealth Extension =head1 VERSION Version 1.63 =head1 SYNOPSIS use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new(stealth => 1); $firefox->go("https://fastapi.metacpan.org/v1/download_url/Firefox::Marionette"); =head1 DESCRIPTION This module contains the Stealth extension. This module should not be used directly. It is required when the 'stealth' parameter is supplied to the L method in L. =head1 SUBROUTINES/METHODS =head2 new Returns a L of the Stealth extension. =head2 user_agent_contents Returns the javascript used to setup a different (or the original) user agent as a string. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Extension::Stealth 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) 2024, 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.63/lib/Firefox/Marionette/Extension/HarExportTrigger.pm0000644000175000017500000013226214763400572025641 0ustar davedavepackage Firefox::Marionette::Extension::HarExportTrigger; use strict; use warnings; our $VERSION = '1.63'; 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.63 =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) 2024, 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.63/lib/Firefox/Marionette/DNS.pm0000644000175000017500000002016714763400572021051 0ustar davedavepackage Firefox::Marionette::DNS; use strict; use warnings; use Exporter(); *import = \&Exporter::import; our @EXPORT_OK = qw( RESOLVE_TYPE_DEFAULT RESOLVE_TYPE_TXT RESOLVE_TYPE_HTTPSSVC RESOLVE_BYPASS_CACHE RESOLVE_CANONICAL_NAME RESOLVE_PRIORITY_MEDIUM RESOLVE_PRIORITY_LOW RESOLVE_SPECULATE RESOLVE_DISABLE_IPV6 RESOLVE_OFFLINE RESOLVE_DISABLE_IPV4 RESOLVE_ALLOW_NAME_COLLISION RESOLVE_DISABLE_TRR RESOLVE_REFRESH_CACHE RESOLVE_TRR_MODE_MASK RESOLVE_TRR_DISABLED_MODE RESOLVE_IGNORE_SOCKS_DNS RESOLVE_IP_HINT RESOLVE_WANT_RECORD_ON_ERROR RESOLVE_DISABLE_NATIVE_HTTPS_QUERY RESOLVE_CREATE_MOCK_HTTPS_RR ALL_DNSFLAGS_BITS ); our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK, ); our $VERSION = '1.63'; sub RESOLVE_TYPE_DEFAULT { return 0 } sub RESOLVE_TYPE_TXT { return 16 } sub RESOLVE_TYPE_HTTPSSVC { return 65 } sub RESOLVE_DEFAULT_FLAGS { return 0 } sub RESOLVE_BYPASS_CACHE { return 1 } sub RESOLVE_CANONICAL_NAME { return 2 } sub RESOLVE_PRIORITY_MEDIUM { return 4 } sub RESOLVE_PRIORITY_LOW { return 8 } sub RESOLVE_SPECULATE { return 16 } sub RESOLVE_DISABLE_IPV6 { return 32 } sub RESOLVE_OFFLINE { return 64 } sub RESOLVE_DISABLE_IPV4 { return 128 } sub RESOLVE_ALLOW_NAME_COLLISION { return 256 } sub RESOLVE_DISABLE_TRR { return 512 } sub RESOLVE_REFRESH_CACHE { return 1024 } sub RESOLVE_TRR_MODE_MASK { return 6144 } sub RESOLVE_TRR_DISABLED_MODE { return 2048 } sub RESOLVE_IGNORE_SOCKS_DNS { return 8192 } sub RESOLVE_IP_HINT { return 16_384 } sub RESOLVE_WANT_RECORD_ON_ERROR { return 65_536 } sub RESOLVE_DISABLE_NATIVE_HTTPS_QUERY { return 131_072 } sub RESOLVE_CREATE_MOCK_HTTPS_RR { return 262_144 } sub ALL_DNSFLAGS_BITS { return 524_287 } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::DNS - Constants for calls to the resolve method =head1 VERSION Version 1.63 =head1 SYNOPSIS use Firefox::Marionette(); use Firefox::Marionette::DNS qw(:all); use v5.10; my $firefox = Firefox::Marionette->new(); foreach my $address ($firefox->resolve('metacpan.org', type => RESOLVE_TYPE_DEFAULT(), flags => RESOLVE_BYPASS_CACHE())) { ... } =head1 DESCRIPTION This module handles the implementation of the Firefox Marionette DNS constants =head1 CONSTANTS =head2 RESOLVE_TYPE_DEFAULT returns the value of RESOLVE_TYPE_DEFAULT, which is 0, this is the standard L/L lookup. =head2 RESOLVE_TYPE_TXT returns the value of RESOLVE_TYPE_TXT, which is 1 << 4 = 16, this is a L lookup. =head2 RESOLVE_TYPE_HTTPSSVC = 65, returns the value of RESOLVE_TYPE_HTTPSSVC, which is 65, this is L. =head2 RESOLVE_DEFAULT_FLAGS returns the value of RESOLVE_DEFAULT_FLAGS, which is 0, this is the default. =head2 RESOLVE_BYPASS_CACHE returns the value of RESOLVE_BYPASS_CACHE, which is 1 << 0 = 1, this suppresses the internal DNS lookup cache. =head2 RESOLVE_CANONICAL_NAME returns the value of RESOLVE_CANONICAL_NAME, which is 1 << 1 = 2, this queries the canonical name of the specified host. =head2 RESOLVE_PRIORITY_MEDIUM returns the value of RESOLVE_PRIORITY_MEDIUM, which is 1 << 2 = 4, this gives the query lower priority. =head2 RESOLVE_PRIORITY_LOW returns the value of RESOLVE_PRIORITY_LOW, which is 1 << 3 = 8, this gives the query lower priority still. =head2 RESOLVE_SPECULATE returns the value of RESOLVE_SPECULATE, which is 1 << 4 = 16, indicates request is speculative. Speculative requests return errors if prefetching is disabled by configuration. =head2 RESOLVE_DISABLE_IPV6 returns the value of RESOLVE_DISABLE_IPV6, which is 1 << 5 = 32, this only returns IPv4 addresses. =head2 RESOLVE_OFFLINE return 64, only literals and cached entries will be returned. =head2 RESOLVE_DISABLE_IPV4 returns 128, only IPv6 addresses will be returned from resolve/asyncResolve. =head2 RESOLVE_ALLOW_NAME_COLLISION returns the value of RESOLVE_ALLOW_NAME_COLLISION, which is 1 << 8 = 256, this allows name collision results (127.0.53.53) which are normally filtered. =head2 RESOLVE_DISABLE_TRR returns the value of RESOLVE_DISABLE_TRR, which is 1 << 9 = 512, this stops using TRR for resolving the host name. =head2 RESOLVE_REFRESH_CACHE returns the value of RESOLVE_REFRESH_CACHE, which is 1 << 10 = 1024, when set (together with L), invalidate the DNS existing cache entry first (if existing) then make a new resolve. =head2 RESOLVE_TRR_MODE_MASK returns the value of RESOLVE_TRR_MODE_MASK, which is ((1 << 11) | (1 << 12)) = 6144, these two bits encode the TRR mode of the request =head2 RESOLVE_TRR_DISABLED_MODE returns the value of RESOLVE_TRR_DISABLED_MODE, which is 1 << 11 = 2048. =head2 RESOLVE_IGNORE_SOCKS_DNS returns the value of RESOLVE_IGNORE_SOCKS_DNS, which is 1 << 13 = 8192, this will orce resolution even when SOCKS proxy with DNS forwarding is configured. Only to be used for the proxy host resolution. =head2 RESOLVE_IP_HINT returns the value of RESOLVE_IP_HINT, which is 1 << 14 = 16384, this will only return cached IP hint addresses from L. =head2 RESOLVE_WANT_RECORD_ON_ERROR returns the value of RESOLVE_WANT_RECORD_ON_ERROR, which is 1 << 16 = 65536, this will pass a DNS record to even when there was a resolution error. =head2 RESOLVE_DISABLE_NATIVE_HTTPS_QUERY returns the value of RESOLVE_DISABLE_NATIVE_HTTPS_QUERY, which is 1 << 17 = 131072, this disables the native HTTPS queries. =head2 RESOLVE_CREATE_MOCK_HTTPS_RR returns the value of RESOLVE_CREATE_MOCK_HTTPS_RR, which is 1 << 18 = 262144, this creates a mock HTTPS RR and use it. This is only for testing purposes =head2 ALL_DNSFLAGS_BITS returns the value of ALL_DNSFLAGS_BITS, which is ((1 << 19) - 1) = 524287, this is all flags turned on. =head1 SUBROUTINES/METHODS None. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::DNS 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) 2024, 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.63/lib/Firefox/Marionette/Display.pm0000644000175000017500000001265614763400572022036 0ustar davedavepackage Firefox::Marionette::Display; use strict; use warnings; our $VERSION = '1.63'; sub new { my ( $class, %parameters ) = @_; my $self = bless {%parameters}, $class; return $self; } sub designation { my ($self) = @_; return $self->{designation}; } sub usage { my ($self) = @_; return $self->{usage}; } sub width { my ($self) = @_; return $self->{width}; } sub height { my ($self) = @_; return $self->{height}; } sub sar { my ($self) = @_; return $self->{sar}; } sub dar { my ($self) = @_; return $self->{dar}; } sub par { my ($self) = @_; return $self->{par}; } sub total { my ($self) = @_; return $self->{width} * $self->{height}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::Display - Represents a display from the displays method =head1 VERSION Version 1.63 =head1 SYNOPSIS use Firefox::Marionette(); use Encode(); use v5.10; my $firefox = Firefox::Marionette->new( visible => 1, kiosk => 1 )->go('http://metacpan.org');; my $element = $firefox->find_id('metacpan_search-input'); foreach my $display ($firefox->displays()) { say 'Can Firefox resize for "' . Encode::encode('UTF-8', $display->usage(), 1) . '"?'; if ($firefox->resize($display->width(), $display->height())) { say 'Now displaying with a Pixel aspect ratio of ' . $display->par(); say 'Now displaying with a Storage aspect ratio of ' . $display->sar(); say 'Now displaying with a Display aspect ratio of ' . $display->dar(); } else { say 'Apparently NOT!'; } } =head1 DESCRIPTION This module handles the implementation of a L. =head1 SUBROUTINES/METHODS =head2 dar returns the L for the display. =head2 designation returns the L value, such as "VGA" or "16K". =head2 height returns the resolution height =head2 new accepts a hash for the display with the following allowed keys; =over 4 =item * designation - See the L column. =item * usage - See the L column. =item * width - The width of the entire firefox window. =item * height - The height of the entire firefox window. =item * sar - The L, which is related to the below ratios with the equation SAR = PAR/DAR. =item * dar - The L. =item * par - The L. =back =head2 par returns the L for the display. =head2 sar returns the L, which is related to the below ratios with the equation SAR = PAR/DAR. =head2 total returns the product of L and L. =head2 usage returns the L value such as "Apple PowerBook G4". =head2 width returns the resolution width =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::Display requires no configuration files or environment variables. =head1 DEPENDENCIES Firefox::Marionette::Display does not requires any 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) 2024, 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.63/lib/Firefox/Marionette/UpdateStatus.pm0000644000175000017500000003025114763400572023046 0ustar davedavepackage Firefox::Marionette::UpdateStatus; use strict; use warnings; use URI(); our $VERSION = '1.63'; 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 'PENDING_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.63 =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) 2024, 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.63/lib/Firefox/Marionette/WebAuthn/0000755000175000017500000000000014763402246021576 5ustar davedaveFirefox-Marionette-1.63/lib/Firefox/Marionette/WebAuthn/Credential.pm0000644000175000017500000002052714763400572024214 0ustar davedavepackage Firefox::Marionette::WebAuthn::Credential; use strict; use warnings; our $VERSION = '1.63'; my %key_mapping = ( isResidentCredential => 'is_resident', rpId => 'host', signCount => 'sign_count', userHandle => 'user', privateKey => 'private_key', credentialId => 'id', ); sub new { my ( $class, %parameters ) = @_; my $self = bless {}, $class; foreach my $key_name ( sort { $a cmp $b } keys %key_mapping ) { if ( exists $parameters{ $key_mapping{$key_name} } ) { $self->{ $key_mapping{$key_name} } = $parameters{ $key_mapping{$key_name} }; } elsif ( exists $parameters{$key_name} ) { $self->{ $key_mapping{$key_name} } = $parameters{$key_name}; } } return $self; } sub id { my ($self) = @_; return $self->{id}; } sub host { my ($self) = @_; return $self->{host}; } sub is_resident { my ($self) = @_; return $self->{is_resident}; } sub private_key { my ($self) = @_; return $self->{private_key}; } sub sign_count { my ($self) = @_; return $self->{sign_count}; } sub user { my ($self) = @_; return $self->{user}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::WebAuthn::Credential - Represents a Firefox WebAuthn Credential =head1 VERSION Version 1.63 =head1 SYNOPSIS use Firefox::Marionette(); use Crypt::URandom(); my $user_name = MIME::Base64::encode_base64( Crypt::URandom::urandom( 10 ), q[] ) . q[@example.com]; my $firefox = Firefox::Marionette->new(); $firefox->go('https://webauthn.io'); $firefox->find_id('input-email')->type($user_name); $firefox->find_id('register-button')->click(); $firefox->await(sub { sleep 1; $firefox->find_class('alert-success'); }); $firefox->find_id('login-button')->click(); $firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); }); foreach my $credential ($firefox->webauthn_credentials()) { $firefox->delete_webauthn_credential($credential); # ... time passes ... $firefox->add_webauthn_credential( id => $credential->id(), host => $credential->host(), user => $credential->user(), private_key => $credential->private_key(), is_resident => $credential->is_resident(), sign_count => $credential->sign_count(), ); } $firefox->go('about:blank'); $firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_COOKIES()); $firefox->go('https://webauthn.io'); $firefox->find_id('input-email')->type($user_name); $firefox->find_id('login-button')->click(); $firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); }); =head1 DESCRIPTION This module handles the implementation of a single WebAuth Credential using the Marionette protocol. =head1 SUBROUTINES/METHODS =head2 new accepts a hash as a parameter. Allowed keys are below; =over 4 =item * host - contains the domain that this credential is to be used for. In the language of L, this field is referred to as the L or L. =item * id - contains the unique id for this credential, also known as the L. =item * is_resident - contains a boolean that if set to true, a L is to be created. If set to false, a L is to be created instead. =item * private_key - either a L encoded private key encoded using L or a hash containing the following keys; =over 8 =item * name - contains the name of the private key algorithm, such as "RSA-PSS" (the default), "RSASSA-PKCS1-v1_5", "ECDSA" or "ECDH". =item * size - contains the modulus length of the private key. This is only valid for "RSA-PSS" or "RSASSA-PKCS1-v1_5" private keys. =item * hash - contains the name of the hash algorithm, such as "SHA-512" (the default). This is only valid for "RSA-PSS" or "RSASSA-PKCS1-v1_5" private keys. =item * curve - contains the name of the curve for the private key, such as "P-384" (the default). This is only valid for "ECDSA" or "ECDH" private keys. =back =item * sign_count - contains the initial value for a L associated to the L. =item * user - contains the L associated to the credential encoded using L. This property is optional. =back This method returns a new L object. =head2 host returns the domain that this credential is to be used for. In the language of L, this field is referred to as the L or L. =head2 is_resident returns a boolean that if true, a L is to be created. If false, a L is to be created instead. =head2 private_key returns a L encoded private key encoded using L. =head2 sign_count returns the L associated to the L. =head2 user returns the L associated to the credential encoded using L. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::WebAuthn::Credential 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) 2024, 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.63/lib/Firefox/Marionette/WebAuthn/Authenticator.pm0000644000175000017500000002330514763400572024751 0ustar davedavepackage Firefox::Marionette::WebAuthn::Authenticator; use strict; use warnings; our $VERSION = '1.63'; sub BLE { return 'ble' } sub CTAP1_U2F { return 'ctap1/u2f' } sub CTAP2 { return 'ctap2' } sub CTAP2_1 { return 'ctap2_1' } sub HYBRID { return 'hybrid' } sub INTERNAL { return 'internal' } sub NFC { return 'nfc' } sub SMART_CARD { return 'smart-card' } sub USB { return 'usb' } sub new { my ( $class, %parameters ) = @_; my $self = bless {%parameters}, $class; return $self; } sub id { my ($self) = @_; return $self->{id}; } sub protocol { my ($self) = @_; return $self->{protocol}; } sub transport { my ($self) = @_; return $self->{transport}; } sub has_resident_key { my ($self) = @_; return $self->{has_resident_key}; } sub has_user_verification { my ($self) = @_; return $self->{has_user_verification}; } sub is_user_consenting { my ($self) = @_; return $self->{is_user_consenting}; } sub is_user_verified { my ($self) = @_; return $self->{is_user_verified}; } 1; # Magic true value required at end of module __END__ =head1 NAME Firefox::Marionette::WebAuthn::Authenticator - Represents a Firefox WebAuthn Authenticator =head1 VERSION Version 1.63 =head1 SYNOPSIS use Firefox::Marionette(); use Crypt::URandom(); my $user_name = MIME::Base64::encode_base64( Crypt::URandom::urandom( 10 ), q[] ) . q[@example.com]; my $firefox = Firefox::Marionette->new(); my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() ); $firefox->go('https://webauthn.io'); $firefox->find_id('input-email')->type($user_name); $firefox->find_id('register-button')->click(); $firefox->await(sub { sleep 1; $firefox->find_class('alert-success'); }); $firefox->find_id('login-button')->click(); $firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); }); =head1 DESCRIPTION This module handles the implementation of a L using the Marionette protocol. =head1 CONSTANTS =head2 BLE return 'ble', the L code for a L transport. =head2 CTAP1_U2F returns 'ctap1/u2f', the L code for an older version of L, that is backwards compatible with the L open standard. =head2 CTAP2 returns 'ctap2', the L code for the L. =head2 CTAP2_1 returns 'ctap2_1', the L code for the next version of the L. =head2 HYBRID returns 'hybrid', the L code for a L transport. =head2 INTERNAL returns 'internal', the L code for an L transport. =head2 NFC return 'nfc', the L code for a L transport. =head2 SMART_CARD returns 'smart-card', the L code for a L L transport. =head2 USB return 'usb', the L code for a L transport. =head1 SUBROUTINES/METHODS =head2 new accepts a hash as a parameter. Allowed keys are below; =over 4 =item * has_resident_key - boolean value to indicate if the L will support L =item * has_user_verification - boolean value to determine if the L supports L. =item * id - the id of the authenticator. =item * is_user_consenting - boolean value to determine the result of all L L, and by extension, any L performed on the L. If set to true, a L will always be granted. If set to false, it will not be granted. =item * is_user_verified - boolean value to determine the result of L performed on the L. If set to true, L will always succeed. If set to false, it will fail. =item * protocol - the L spoken by the authenticator. This may be L, L or L. =item * transport - the L simulated by the authenticator. This may be L, L, L, L, L or L. =back This method returns a new L object. =head2 has_resident_key This method returns a boolean value to indicate if the L will support L. =head2 has_user_verification This method returns a boolean value to determine if the L supports L. =head2 is_user_consenting This method returns a boolean value to determine the result of all L L, and by extension, any L performed on the L. If set to true, a L will always be granted. If set to false, it will not be granted. =head2 is_user_verified This method returns a boolean value to determine the result of L performed on the L. If set to true, L will always succeed. If set to false, it will fail. =head2 protocol This method returns a string containing the L spoken by the authenticator. This may be L, L or L. =head2 transport This method returns a string containing the L simulated by the authenticator. This may be L, L, L, L, L or L. =head1 DIAGNOSTICS None. =head1 CONFIGURATION AND ENVIRONMENT Firefox::Marionette::WebAuthn::Authenticator 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) 2024, 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.63/lib/Firefox/Marionette/Link.pm0000644000175000017500000001146314763400572021321 0ustar davedavepackage Firefox::Marionette::Link; use strict; use warnings; use URI::URL(); use parent qw(Firefox::Marionette::Element); our $VERSION = '1.63'; 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.63 =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 subclass 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) 2024, 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.63/lib/Firefox/Marionette/ShadowRoot.pm0000644000175000017500000000625614763400572022521 0ustar davedavepackage Firefox::Marionette::ShadowRoot; use strict; use warnings; use parent qw(Firefox::Marionette::LocalObject); our $VERSION = '1.63'; sub IDENTIFIER { return 'shadow-6066-11e4-a52e-4f735466cecf' } 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.63 =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 CONSTANTS =head2 IDENTIFIER returns the L =head1 SUBROUTINES/METHODS =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) 2024, 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.63/lib/Firefox/Marionette.pm0000644000175000017500000213165714763400572020436 0ustar davedavepackage Firefox::Marionette; use warnings; use strict; use Firefox::Marionette::Response(); use Firefox::Marionette::Bookmark(); use Firefox::Marionette::Element(); use Firefox::Marionette::Cache(); use Firefox::Marionette::Cookie(); use Firefox::Marionette::Display(); use Firefox::Marionette::DNS(); use Firefox::Marionette::Window::Rect(); use Firefox::Marionette::Element::Rect(); use Firefox::Marionette::GeoLocation(); 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 Firefox::Marionette::WebAuthn::Authenticator(); use Firefox::Marionette::WebAuthn::Credential(); use Firefox::Marionette::WebFrame(); use Firefox::Marionette::WebWindow(); use Waterfox::Marionette::Profile(); use Compress::Zlib(); use Config::INI::Reader(); use Crypt::URandom(); use Archive::Zip(); use Symbol(); use JSON(); use IO::Handle(); 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 Time::Local(); use File::HomeDir(); use File::Temp(); 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 parent 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.63'; 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 _MAX_VERSION_NO_POINTER_ORIGIN { return 116 } sub _MIN_VERSION_FOR_AWAIT { return 50 } 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_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 _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 _MIN_VERSION_FOR_MODERN_GO { return 31 } sub _MIN_VERSION_FOR_MODERN_SWITCH { return 90 } sub _MIN_VERSION_FOR_WEBAUTHN { return 118 } sub _ACTIVE_UPDATE_XML_FILE_NAME { return 'active-update.xml' } sub _NUMBER_OF_CHARS_IN_TEMPLATE { return 11 } sub _DEFAULT_ADB_PORT { return 5555 } sub _SHORT_GUID_BYTES { return 9 } sub _DEFAULT_DOWNLOAD_TIMEOUT { return 300 } sub _CREDENTIAL_ID_LENGTH { return 32 } sub _FIREFOX_109_RV_MIN { return 109; } # https://bugzilla.mozilla.org/show_bug.cgi?id=1805967 sub _FIREFOX_109_RV_MAX { return 119; } # https://bugzilla.mozilla.org/show_bug.cgi?id=1805967 # 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 sub BCD_PATH { my ($create) = @_; my $directory = File::HomeDir->my_dist_data( 'Firefox-Marionette', { defined $create && $create == 1 ? ( create => 1 ) : () } ); if ( defined $directory ) { return File::Spec->catfile( $directory, 'bcd.json' ); } else { return } } my $proxy_name_regex = qr/perl_ff_m_\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}; my $webauthn_default_authenticator_key_name = '_webauthn_authenticator'; 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 languages { my ( $self, @new_languages ) = @_; my $pref_name = 'intl.accept_languages'; my $script = 'return navigator.languages || branch.getComplexValue(arguments[0], Components.interfaces.nsIPrefLocalizedString).data.split(/,\s*/)'; my $old = $self->_context('chrome'); my @old_languages = @{ $self->script( $self->_compress_script( $self->_prefs_interface_preamble() . $script ), args => [$pref_name] ) }; $self->_context($old); if ( scalar @new_languages ) { $self->set_pref( $pref_name, join q[, ], @new_languages ); } return @old_languages; } sub _setup_trackable { my ( $self, $trackable ) = @_; my $value = $trackable ? 0 : 1; $self->set_pref( 'privacy.fingerprintingProtection', $value ); $self->set_pref( 'privacy.fingerprintingProtection.pbmode', $value ); return $self; } sub _setup_geo { my ( $self, $geo ) = @_; $self->set_pref( 'geo.enabled', 1 ); $self->set_pref( 'geo.provider.use_geoclue', 0 ); $self->set_pref( 'geo.provider.use_corelocation', 0 ); $self->set_pref( 'geo.provider.testing', 1 ); $self->set_pref( 'geo.prompt.testing', 1 ); $self->set_pref( 'geo.prompt.testing.allow', 1 ); $self->set_pref( 'geo.security.allowinsecure', 1 ); $self->set_pref( 'geo.wifi.scan', 1 ); $self->set_pref( 'permissions.default.geo', 1 ); if ( ref $geo ) { if ( ( Scalar::Util::blessed($geo) ) && ( $geo->isa('URI') ) ) { $self->geo( $self->json($geo) ); } else { $self->geo($geo); } } elsif ( $geo =~ /^(?:data|http)/smx ) { $self->geo( $self->json($geo) ); } return $self; } sub tz { my ( $self, $timezone ) = @_; require Firefox::Marionette::Extension::Timezone; my %parameters = ( timezone => $timezone ); $self->script( $self->_compress_script( Firefox::Marionette::Extension::Timezone->timezone_contents( %parameters) ) ); if ( $self->{timezone_extension} ) { $self->uninstall( delete $self->{timezone_extension} ); } my $zip = Firefox::Marionette::Extension::Timezone->new(%parameters); $self->{timezone_extension} = $self->_install_extension_by_handle( $zip, 'timezone-0.0.1.xpi' ); return $self; } sub geo { my ( $self, @parameters ) = @_; my $location; if ( scalar @parameters ) { $location = Firefox::Marionette::GeoLocation->new(@parameters); } if ( defined $location ) { $self->set_pref( 'geo.provider.network.url', q[data:application/json,] . JSON->new()->convert_blessed()->encode($location) ); $self->set_pref( 'geo.wifi.uri', q[data:application/json,] . JSON->new()->convert_blessed()->encode($location) ); if ( my $ipgeolocation_timezone = $location->tz() ) { $self->tz($ipgeolocation_timezone); } return $self; } if ( my $geo_location = $self->_get_geolocation() ) { my $new_location = Firefox::Marionette::GeoLocation->new($geo_location); return $new_location; } return; } sub _get_geolocation { my ($self) = @_; my $result = $self->script( $self->_compress_script( <<'_JS_') ); return (async function() { function getGeo() { return new Promise((resolve, reject) => { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(resolve, reject, { maximumAge: 0, enableHighAccuracy: true }); } else { reject("navigator.geolocation is unavailable"); } }) }; return await getGeo().then((response) => { let d = new Date(); return { "timezone_offset": d.getTimezoneOffset(), "latitude": response["coords"]["latitude"], "longitude": response["coords"]["longitude"], "altitude": response["coords"]["altitude"], "accuracy": response["coords"]["accuracy"], "altitudeAccuracy": response["coords"]["altitudeAccuracy"], "heading": response["coords"]["heading"], "speed": response["coords"]["speed"], }; }).catch((err) => { throw err.message }); })(); _JS_ if ( ( defined $result ) && ( !ref $result ) ) { Firefox::Marionette::Exception->throw("javascript error: $result"); } return $result; } sub _prefs_interface_preamble { my ($self) = @_; return <<'_JS_'; # modules/libpref/nsIPrefService.idl let prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService); let branch = prefs.getBranch(""); _JS_ } sub get_pref { my ( $self, $name ) = @_; my $script = <<'_JS_'; let result = [ null ]; switch (branch.getPrefType(arguments[0])) { case branch.PREF_STRING: result = [ branch.getStringPref ? branch.getStringPref(arguments[0]) : branch.getComplexValue(arguments[0], Components.interfaces.nsISupportsString).data, 'string' ]; break; case branch.PREF_INT: result = [ branch.getIntPref(arguments[0]), 'integer' ]; break; case branch.PREF_BOOL: result = [ branch.getBoolPref(arguments[0]), 'boolean' ]; } return result; _JS_ my $old = $self->_context('chrome'); my ( $result, $type ) = @{ $self->script( $self->_compress_script( $self->_prefs_interface_preamble() . $script ), args => [$name] ) }; $self->_context($old); if ($type) { if ( $type eq 'integer' ) { $result += 0; } } return $result; } sub set_pref { my ( $self, $name, $value ) = @_; my $script = <<'_JS_'; switch (branch.getPrefType(arguments[0])) { case branch.PREF_INT: branch.setIntPref(arguments[0], arguments[1]); break; case branch.PREF_BOOL: branch.setBoolPref(arguments[0], arguments[1] ? true : false); break; case branch.PREF_STRING: default: if (branch.setStringPref) { branch.setStringPref(arguments[0], arguments[1]); } else { let newString = Components.classes["@mozilla.org/supports-string;1"].createInstance(Components.interfaces.nsISupportsString); newString.data = arguments[1]; branch.setComplexValue(arguments[0], Components.interfaces.nsISupportsString, newString); } } _JS_ my $old = $self->_context('chrome'); $self->script( $self->_compress_script( $self->_prefs_interface_preamble() . $script ), args => [ $name, $value ] ); $self->_context($old); return $self; } sub _clear_data_service_interface_preamble { my ($self) = @_; return <<'_JS_'; # toolkit/components/cleardata/nsIClearDataService.idl let clearDataService = Components.classes["@mozilla.org/clear-data-service;1"].getService(Components.interfaces.nsIClearDataService); _JS_ } sub cache_keys { my ($self) = @_; my @names; foreach my $name (@Firefox::Marionette::Cache::EXPORT_OK) { if ( defined $self->check_cache_key($name) ) { push @names, $name; } } return @names; } sub check_cache_key { my ( $self, $name ) = @_; my $class = ref $self; defined $name or Firefox::Marionette::Exception->throw( "$class->check_cache_value() must be passed an argument."); $name =~ /^[[:upper:]_]+$/smx or Firefox::Marionette::Exception->throw( "$class->check_cache_key() must be passed an argument consisting of uppercase characters and underscores." ); my $script = <<"_JS_"; if (typeof clearDataService.$name === undefined) { return; } else { return clearDataService.$name; } _JS_ my $old = $self->_context('chrome'); my $result = $self->script( $self->_compress_script( $self->_clear_data_service_interface_preamble() . $script ) ); $self->_context($old); return $result; } sub clear_cache { my ( $self, $flags ) = @_; $flags = defined $flags ? $flags : Firefox::Marionette::Cache::CLEAR_ALL(); my $script = <<'_JS_'; let argument_flags = arguments[0]; let clearCache = function(flags) { return new Promise((resolve) => { clearDataService.deleteData(flags, function() { resolve(); }); })}; let result = (async function() { let awaitResult = await clearCache(argument_flags); return awaitResult; })(); return arguments[0]; _JS_ my $old = $self->_context('chrome'); my $result = $self->script( $self->_compress_script( $self->_clear_data_service_interface_preamble() . $script ), args => [$flags] ); $self->_context($old); return $self; } sub clear_pref { my ( $self, $name ) = @_; my $script = <<'_JS_'; branch.clearUserPref(arguments[0]); _JS_ my $old = $self->_context('chrome'); $self->script( $self->_compress_script( $self->_prefs_interface_preamble() . $script ), args => [$name] ); $self->_context($old); return $self; } sub _is_chrome_user_agent { my ( $self, $user_agent ) = @_; if ( $user_agent =~ /Chrome/smx ) { return 1; } return; } sub _is_safari_user_agent { my ( $self, $user_agent ) = @_; if ( $user_agent =~ /Safari/smx ) { return 1; } return; } sub _is_safari_and_iphone_user_agent { my ( $self, $user_agent ) = @_; if ( $user_agent =~ /iPhone/smx ) { return 1; } return; } sub _is_trident_user_agent { my ( $self, $user_agent ) = @_; if ( $user_agent =~ /Trident/smx ) { return 1; } return; } sub _parse_user_agent { my ( $self, $user_agent ) = @_; my ( $app_version, $platform, $product, $product_sub, $vendor, $vendor_sub, $oscpu ); # https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgent#value if ( !defined $user_agent ) { $user_agent = $self->_original_agent(); } if ( $user_agent =~ m{^ [^\/]+\/ # appCodeName ((5[.]0[ ][(][^; ]+)[^;]*;[ ] # appVersion ([^;)]+)[;)] # platform .*) $}smx ) { ( my $webkit_app, $app_version, $platform ) = ( $1, $2, $3 ); $app_version .= q[)]; ( $vendor, $vendor_sub, $oscpu ) = ( q[], q[], $platform ); $product = 'Gecko'; $product_sub = '20100101'; if ( $self->_is_chrome_user_agent($user_agent) ) { $app_version = $webkit_app; $product_sub = '20030107'; $vendor = 'Google Inc.'; $vendor_sub = q[]; $oscpu = undef; } elsif ( $self->_is_safari_user_agent($user_agent) ) { $app_version = $webkit_app; $product_sub = '20030107'; if ( $self->_is_safari_and_iphone_user_agent($user_agent) ) { $platform = 'iPhone'; } else { $platform = 'MacIntel'; } $vendor = 'Apple Computer, Inc.'; $vendor_sub = q[]; $oscpu = undef; } elsif ( $self->_is_trident_user_agent($user_agent) ) { $app_version = $webkit_app; $product_sub = undef; $vendor = q[]; $vendor_sub = undef; } if ( $user_agent =~ /Win(?:32|64|dows)/smx ) { $platform = 'Win32'; if ( $self->_is_chrome_user_agent($user_agent) ) { $oscpu = undef; } elsif ( $self->_is_trident_user_agent($user_agent) ) { $oscpu = undef; } else { $oscpu = $platform; } } elsif ( $user_agent =~ /Intel[ ]Mac/smx ) { $platform = 'MacIntel'; } elsif ( $user_agent =~ /Android/smx ) { $platform = 'Linux armv81'; $oscpu = undef; } } else { ( $app_version, $platform, $product, $product_sub, $vendor, $vendor_sub, $oscpu ) = ( q[], q[], q[], q[], q[], q[], q[] ); } return ( $user_agent, $app_version, $platform, $product, $product_sub, $vendor, $vendor_sub, $oscpu ); } sub _original_agent { my ($self) = @_; return $self->{original_agent}; } sub _parse_original_agent { my ($self) = @_; my $original_string = $self->_original_agent(); my $general_token_re = qr/(?:Mozilla\/5[.]0[ ])[(]/smx; my $platform_re = qr/([^)]*?);[ ]/smx; my $gecko_version_re = qr/rv:(\d+)[.]0[)][ ]/smx; my $gecko_trail_re = qr/Gecko\/20100101[ ]/smx; my $firefox_version_re = qr/Firefox\/(\d+)[.]0/smx; my ( $os_string, $rv_version, $firefox_version ); if ( $original_string =~ m/^$general_token_re$platform_re$gecko_version_re$gecko_trail_re$firefox_version_re$/smx ) { ( $os_string, $rv_version, $firefox_version ) = ( $1, $2, $3 ); } else { Firefox::Marionette::Exception->throw( 'Failed to parse user agent:' . $original_string ); } return ( $os_string, $rv_version, $firefox_version ); } sub _get_agent_from_hash { my ( $self, %new_hash ) = @_; my $new_agent; my ( $os_string, $rv_version, $firefox_version ) = $self->_parse_original_agent(); $rv_version = $firefox_version; if ( $new_hash{increment} ) { if ( $new_hash{increment} =~ /^\s*([-])?\s*(\d{1,3})\s*$/smx ) { my ( $sign, $number ) = ( $1, $2 ); my $increment = int "$sign$number"; $rv_version += $increment; $firefox_version += $increment; delete $new_hash{increment}; } else { Firefox::Marionette::Exception->throw( 'The increment parameter for the agent method must be a positive or negative integer less than 1000.' ); } } if ( $new_hash{version} ) { if ( $new_hash{version} =~ /^\s*(\d{1,3})\s*$/smx ) { my ($version) = ($1); $rv_version = $version; $firefox_version = $version; delete $new_hash{version}; } else { Firefox::Marionette::Exception->throw( 'The version parameter for the agent method must be a positive less than 1000.' ); } } if ( ( $rv_version >= _FIREFOX_109_RV_MIN() ) && ( $rv_version <= _FIREFOX_109_RV_MAX() ) ) { $rv_version = _FIREFOX_109_RV_MIN(); } if ( my $os = $new_hash{os} ) { my %correct_os = ( linux => 'Linux', freebsd => 'FreeBSD', openbsd => 'OpenBSD', netbsd => 'NetBSD', dragonfly => 'DragonFly', win32 => 'Win64', # https://bugzilla.mozilla.org/show_bug.cgi?id=1559747 win64 => 'Win64', mac => 'Intel Mac OS X', darwin => 'Intel Mac OS X', ); my %default_platform = ( linux => 'X11', freebsd => 'X11', openbsd => 'X11', netbsd => 'X11', dragonfly => 'X11', win32 => 'Windows NT 10.0', win64 => 'Windows NT 10.0', mac => 'Macintosh', darwin => 'Macintosh', ); my %default_arch = ( linux => 'x86_64', netbsd => 'amd64', freebsd => 'amd64', openbsd => 'amd64', dragonfly => 'x86_64', win32 => 'x64', # https://bugzilla.mozilla.org/show_bug.cgi?id=1559747 win64 => 'x64', mac => '14.3', # try and keep to a recent release darwin => '14.3', # try and keep to a recent release ); my $final_platform = $default_platform{ lc $os }; if ( defined $new_hash{platform} ) { $final_platform = $new_hash{platform}; } my $final_os = $correct_os{ lc $os }; my $final_arch = $default_arch{ lc $os }; if ( defined $new_hash{arch} ) { $final_arch = $new_hash{arch}; } $os_string = join q[; ], $final_platform, $self->_join_os_arch_in_agent( $final_os, $final_arch ); delete $new_hash{os}; } $new_agent = 'Mozilla/5.0 (' . $os_string . '; rv:' . $rv_version . '.0) Gecko/20100101 Firefox/' . $firefox_version . '.0'; return $new_agent; } sub _join_os_arch_in_agent { my ( $self, $os, $arch ) = @_; if ( $os =~ /^Win(?:32|64)$/smx ) { return join q[; ], $os, $arch; } else { return join q[ ], $os, $arch; } } sub agent { my ( $self, @new_list ) = @_; my $pref_name = 'general.useragent.override'; my $old_agent = $self->script( $self->_compress_script('return navigator.userAgent') ); if ( !defined $self->_original_agent() ) { $self->{original_agent} = $old_agent; } if ( ( scalar @new_list ) > 0 ) { my $new_agent; if ( !( ( scalar @new_list ) % 2 ) ) { $new_agent = $self->_get_agent_from_hash(@new_list); } else { $new_agent = $new_list[0]; } my ( $user_agent, $app_version, $platform, $product, $product_sub, $vendor, $vendor_sub, $oscpu ) = $self->_parse_user_agent($new_agent); $self->set_pref( $pref_name, $user_agent ); $self->set_pref( 'general.platform.override', $platform ); $self->set_pref( 'general.appversion.override', $app_version ); $self->set_pref( 'general.oscpu.override', $oscpu ); if ( $self->_is_chrome_user_agent($user_agent) ) { $self->set_pref( 'network.http.accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' ); $self->set_pref( 'network.http.accept-encoding', 'gzip, deflate, br' ); $self->set_pref( 'network.http.accept-encoding.secure', 'gzip, deflate, br' ); } elsif ( $self->_is_safari_user_agent($user_agent) ) { $self->set_pref( 'network.http.accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' ); $self->set_pref( 'network.http.accept-encoding', 'gzip, deflate, br' ); $self->set_pref( 'network.http.accept-encoding.secure', 'gzip, deflate, br' ); } elsif ( $self->_is_trident_user_agent($user_agent) ) { # https://stackoverflow.com/questions/1670329/ie-accept-headers-changing-why $self->set_pref( 'network.http.accept', 'image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/msword, application/vnd.ms-excel, application/x-shockwave-flash, */*', ); $self->set_pref( 'network.http.accept-encoding', 'gzip, deflate' ); $self->set_pref( 'network.http.accept-encoding.secure', 'gzip, deflate' ); } else { $self->clear_pref('network.http.accept'); $self->clear_pref('network.http.accept-encoding'); $self->clear_pref('network.http.accept-encoding.secure'); } my $false = $self->_translate_to_json_boolean(0); $self->set_pref( 'privacy.donottrackheader.enabled', $false ) ; # trying to blend in with the most common options if ( $self->{stealth} ) { my %agent_parameters = ( from => $old_agent, to => $user_agent, app_version => $app_version, platform => $platform, product => $product, product_sub => $product_sub, vendor => $vendor, vendor_sub => $vendor_sub, oscpu => $oscpu, ); $self->script( $self->_compress_script( Firefox::Marionette::Extension::Stealth ->user_agent_contents( %agent_parameters) ) ); $self->uninstall( delete $self->{stealth_extension} ); my $zip = Firefox::Marionette::Extension::Stealth->new(%agent_parameters); $self->{stealth_extension} = $self->_install_extension_by_handle( $zip, 'stealth-0.0.1.xpi' ); } } return $old_agent; } sub _download_directory { my ($self) = @_; my $directory = $self->get_pref('browser.download.downloadDir'); if ( my $ssh = $self->_ssh() ) { } elsif ( $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, $url, $default_timeout ) = @_; my $download_directory = $self->_download_directory(); my $quoted_download_directory = quotemeta $download_directory; if ( $url =~ /^$quoted_download_directory/smx ) { my $path = $url; Carp::carp( '**** DEPRECATED - The download(' . q[$] . 'path) method HAS BEEN REPLACED BY downloaded(' . q[$] . 'path) ****' ); return $self->downloaded($path); } else { $default_timeout ||= _DEFAULT_DOWNLOAD_TIMEOUT(); $default_timeout *= _MILLISECONDS_IN_ONE_SECOND(); my $uri = URI->new($url); my $download_name = File::Temp::mktemp('firefox_marionette_download_XXXXXXXXXXX'); my $download_path = File::Spec->catfile( $download_directory, $download_name ); my $timeouts = $self->timeouts(); $self->chrome()->timeouts( Firefox::Marionette::Timeouts->new( script => $default_timeout, implicit => $timeouts->implicit(), page_load => $timeouts->page_load() ) ); my $original_script = $timeouts->script(); my $result = $self->script( $self->_compress_script( <<'_SCRIPT_'), args => [ $uri->as_string(), $download_path ] ); let lazy = {}; if (ChromeUtils.defineESModuleGetters) { ChromeUtils.defineESModuleGetters(lazy, { Downloads: "resource://gre/modules/Downloads.sys.mjs", }); } else { lazy.Downloads = ChromeUtils.import("resource://gre/modules/Downloads.jsm").Downloads; } return Downloads.fetch({ url: arguments[0] }, { path: arguments[1] }); _SCRIPT_ $self->timeouts($timeouts); $self->content(); my $handle; while ( !$handle ) { foreach my $downloaded_path ( $self->downloads($download_directory) ) { if ( $downloaded_path eq $download_path ) { $handle = $self->downloaded($downloaded_path); } } } return $handle; } } sub set_javascript { my ( $self, $value ) = @_; my $pref_name = 'javascript.enabled'; if ( defined $value ) { $self->set_pref( $pref_name, $self->_translate_to_json_boolean( $value ? 1 : 0 ) ); } else { $self->clear_pref($pref_name); } return $self; } sub downloaded { 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 ( defined( 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 ); } } closedir $handle 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, $download_directory ) = @_; $download_directory ||= $self->_download_directory(); return $self->_directory_listing( {}, $download_directory ); } sub resolve_override { my ( $self, $host_name, $ip_address ) = @_; my $old = $self->_context('chrome'); my $result = $self->script( $self->_compress_script( <<'_JS_'), args => [ $host_name, $ip_address ] ); const override = Components.classes["@mozilla.org/network/native-dns-override;1"].getService(Components.interfaces.nsINativeDNSResolverOverride); override.addIPOverride(arguments[0], arguments[1]); _JS_ $self->_context($old); return $self; } sub resolve { my ( $self, $host_name, %options ) = @_; my $old = $self->_context('chrome'); my $type = defined $options{type} ? $options{type} : Firefox::Marionette::DNS::RESOLVE_TYPE_DEFAULT(); my $flags = defined $options{flags} ? $options{flags} : Firefox::Marionette::DNS::RESOLVE_DEFAULT_FLAGS(); my $result = $self->script( $self->_compress_script( <<'_JS_'), args => [ $host_name, $type, $flags, undef ] ); let lookup = function(host_name, type, flags) { return new Promise((resolve) => { let listener = { QueryInterface: function(aIID) { if (aIID.equals(Components.interfaces.nsIDNSListener) || aIID.equals(Components.interfaces.nsISupports)) { return this; } throw Components.results.NS_NOINTERFACE; }, onLookupComplete: function(inRequest, inRecord, inStatus) { if (Components.interfaces.nsIDNSAddrRecord) { inRecord.QueryInterface(Components.interfaces.nsIDNSAddrRecord); } let answers = new Array(); while(inRecord.hasMore()) { answers.push(inRecord.getNextAddrAsString()); } resolve(answers); } }; let threadManager = Components.classes["@mozilla.org/thread-manager;1"].createInstance(Components.interfaces.nsIThreadManager); let currentThread = threadManager.currentThread; let dnsService = Components.classes["@mozilla.org/network/dns-service;1"].createInstance(Components.interfaces.nsIDNSService); try { dnsService.asyncResolve(arguments[0], arguments[2], listener, currentThread); } catch (e) { dnsService.asyncResolve(arguments[0], arguments[1], arguments[2], null, listener, currentThread); } })}; let result = (async function(host_name, type, flag) { let awaitResult = await lookup(host_name, type, flag); return awaitResult; })(arguments[0], arguments[1], arguments[2], arguments[3]); return result; _JS_ $self->_context($old); return @{$result}; } sub _setup_adb { my ( $self, $host, $port ) = @_; if ( !defined $port ) { $port = _DEFAULT_ADB_PORT(); } $self->{_adb} = { host => $host, port => $port }; 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; } } } closedir $directory_handle 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}; $self->{_remote_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; } } } closedir $temp_handle 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(); if ( ( defined $self->_visible() ) && ( $self->_visible() eq 'local' ) ) { if ( !$self->_get_remote_environment_variable_via_ssh('DISPLAY') ) { Firefox::Marionette::Exception->throw( $self->_ssh_address() . ' is not allowing X11 Forwarding' ); } } 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' ); next if ( $key eq 'geo' ); $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}; $self->{force_scp_protocol} = $parameters{scp}; $self->{visible} = $parameters{visible}; $self->{force_webauthn} = $parameters{webauthn}; $self->{geo} = $parameters{geo}; 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->{ssh_via_host} = $parameters{via}; $self->{reconnect_index} = $parameters{index}; $self->_get_marionette_parameter(%parameters); if ( $parameters{console} ) { $self->{console} = 1; } if ( defined $parameters{adb} ) { $self->_setup_adb( $parameters{adb}, $parameters{port} ); } if ( defined $parameters{host} ) { if ( $OSNAME eq 'MSWin32' ) { $parameters{user} ||= Win32::LoginName(); } else { $parameters{user} ||= getpwuid $EFFECTIVE_USER_ID; } if ( $parameters{host} =~ s/:(\d+)$//smx ) { $parameters{port} = $1; } $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{trackable} ) { $self->{trackable} = $parameters{trackable}; } if ( defined $parameters{timezone} ) { $self->{timezone} = $parameters{timezone}; } $self->_load_specified_extensions(%parameters); $self->_determine_mime_types(%parameters); return $self; } sub _load_specified_extensions { my ( $self, %parameters ) = @_; if ( defined $parameters{har} ) { $self->{_har} = $parameters{har}; require Firefox::Marionette::Extension::HarExportTrigger; } if ( $parameters{stealth} ) { $self->{stealth} = 1; require Firefox::Marionette::Extension::Stealth; } return; } sub _determine_mime_types { my ( $self, %parameters ) = @_; $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; } 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_name_regex { my ($self) = @_; my $local_name_regex = qr/firefox_marionette_local_/smx; if ( $self->{reconnect_index} ) { my $quoted_index = quotemeta $self->{reconnect_index}; $local_name_regex = qr/${local_name_regex}${quoted_index}\-/smx; } $local_name_regex = qr/${local_name_regex}\w+/smx; return $local_name_regex; } 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; my $local_name_regex = $self->_get_local_name_regex(); 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; $self->_setup_profile(); } } closedir $temp_handle or Firefox::Marionette::Exception->throw( "Failed to close directory '$temp_directory':$EXTENDED_OS_ERROR"); return $alive_pid; } sub _setup_profile { my ($self) = @_; 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' ); } return; } 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_ my $old = $self->_context('chrome'); my $result = $self->script( $self->_compress_script( $self->_pk11_tokendb_interface_preamble() . $script ) ); $self->_context($old); return $result; } sub pwd_mgr_logout { my ($self) = @_; my $script = <<'_JS_'; token.logoutAndDropAuthenticatedResources(); _JS_ my $old = $self->_context('chrome'); $self->script( $self->_compress_script( $self->_pk11_tokendb_interface_preamble() . $script ) ); $self->_context($old); 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_ my $old = $self->_context('chrome'); $self->script( $self->_compress_script( $self->_pk11_tokendb_interface_preamble() . $script ), args => [$password] ); $self->_context($old); 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_ my $old = $self->_context('chrome'); if ( $self->script( $self->_compress_script( $self->_pk11_tokendb_interface_preamble() . $script ), args => [$password] ) ) { $self->_context($old); } else { $self->_context($old); Firefox::Marionette::Exception->throw('Incorrect Primary Password'); } return $self; } sub arch { my ($self) = @_; my $old = $self->_context('chrome'); my $arch = $self->script(<<'_JS_'); return Services.appinfo.XPCOMABI; _JS_ $arch =~ s/\-.*+$//smx; # stripping suffixes like x86_64-gcc3 $self->_context($old); return $arch; } sub _bookmark_interface_preamble { my ($self) = @_; # toolkit/components/places/nsITaggingService.idl # netwerk/base/NetUtil.sys.mjs # toolkit/components/places/PlacesUtils.sys.mjs return <<'_JS_'; # toolkit/components/places/Bookmarks.sys.mjs let lazy = {}; if (ChromeUtils.defineESModuleGetters) { ChromeUtils.defineESModuleGetters(lazy, { Bookmarks: "resource://gre/modules/Bookmarks.sys.mjs", PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", NetUtil: "resource://gre/modules/NetUtil.sys.mjs", }); } else { lazy.Bookmarks = ChromeUtils.import("resource://gre/modules/Bookmarks.jsm").Bookmarks; lazy.PlacesUtils = ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm").PlacesUtils; lazy.NetUtil = ChromeUtils.import("resource://gre/modules/NetUtil.jsm").NetUtil; } let taggingSvc = Components.classes["@mozilla.org/browser/tagging-service;1"].getService(Components.interfaces.nsITaggingService); _JS_ } sub _get_bookmark_mapping { my ($self) = @_; my %mapping = ( url => 'url', guid => 'guid', parent_guid => 'parentGuid', index => 'index', guid_prefix => 'guidPrefix', icon_url => 'iconUri', icon => 'icon', tags => 'tags', type => 'typeCode', date_added => 'dateAdded', last_modified => 'lastModified', ); return %mapping; } sub _map_bookmark_parameter { my ( $self, $parameter ) = @_; if ( ref $parameter ) { my %mapping = $self->_get_bookmark_mapping(); foreach my $key ( sort { $a cmp $b } keys %mapping ) { if ( exists $parameter->{$key} ) { $parameter->{ $mapping{$key} } = delete $parameter->{$key}; if ( ( $key eq 'icon_url' ) || ( $key eq 'icon' ) || ( $key eq 'url' ) ) { $parameter->{ $mapping{$key} } = ref $parameter->{$key} ? $parameter->{$key}->as_string() : $parameter->{$key}; } } } } return $parameter; } sub _get_bookmark { my ( $self, $parameter ) = @_; $parameter = $self->_map_bookmark_parameter($parameter); my $old = $self->_context('chrome'); my $result = $self->script( $self->_compress_script( $self->_bookmark_interface_preamble() . <<'_JS_'), args => [$parameter] ); return (async function(guidOrInfo) { let bookmark = await lazy.Bookmarks.fetch(guidOrInfo); if (bookmark) { for(let name of [ "dateAdded", "lastModified" ]) { bookmark[name] = Math.floor(bookmark[name] / 1000); } } if ((bookmark) && ("url" in bookmark)) { let keyword = await lazy.PlacesUtils.keywords.fetch({ "url": bookmark["url"] }); if (keyword) { bookmark["keyword"] = keyword["keyword"]; } let url = lazy.NetUtil.newURI(bookmark["url"]); bookmark["tags"] = await lazy.PlacesUtils.tagging.getTagsForURI(url); let addFavicon = function(pageUrl) { return new Promise((resolve, reject) => { PlacesUtils.favicons.getFaviconDataForPage( pageUrl, function (pageUrl, dataLen, data, mimeType, size) { resolve([ pageUrl, dataLen, data, mimeType, size ]); } ); })}; let awaitResult = await addFavicon(lazy.PlacesUtils.toURI(bookmark["url"])); if (awaitResult[0]) { bookmark["iconUrl"] = awaitResult[0].spec; } let iconAscii = btoa(String.fromCharCode(...new Uint8Array(awaitResult[2]))); if (iconAscii) { bookmark["icon"] = "data:" + awaitResult[3] + ";base64," + iconAscii; } } return bookmark; })(arguments[0]); _JS_ $self->_context($old); my $bookmark; if ( defined $result ) { $bookmark = Firefox::Marionette::Bookmark->new( %{$result} ); } return $bookmark; } sub bookmarks { my ( $self, @parameters ) = @_; my $parameter; if ( scalar @parameters >= 2 ) { my %parameters = @parameters; $parameter = \%parameters; } else { $parameter = shift @parameters; } if ( !defined $parameter ) { $parameter = {}; } $parameter = $self->_map_bookmark_parameter($parameter); my $old = $self->_context('chrome'); my @bookmarks = map { $self->_get_bookmark( $_->{guid} ) } @{ $self->script( $self->_compress_script( $self->_bookmark_interface_preamble() . <<'_JS_'), args => [$parameter] ) }; return lazy.Bookmarks.search(arguments[0]); _JS_ $self->_context($old); return @bookmarks; } sub add_bookmark { my ( $self, $bookmark ) = @_; my $old = $self->_context('chrome'); $self->script( $self->_compress_script( $self->_bookmark_interface_preamble() . <<'_JS_'), args => [$bookmark] ); for(let name of [ "dateAdded", "lastModified" ]) { if (arguments[0][name]) { arguments[0][name] = new Date(parseInt(arguments[0][name] + "000", 10)); } } if (arguments[0]["tags"]) { let tags = arguments[0]["tags"]; delete arguments[0]["tags"]; let url = lazy.NetUtil.newURI(arguments[0]["url"]); taggingSvc.tagURI(url, tags); } if (arguments[0]["keyword"]) { let keyword = arguments[0]["keyword"]; delete arguments[0]["keyword"]; let url = arguments[0]["url"]; lazy.PlacesUtils.keywords.insert({ "url": url, "keyword": keyword }); } let bookmarkStatus = (async function(bookmarkArguments) { let exists = await lazy.Bookmarks.fetch({ "guid": bookmarkArguments["guid"] }); let bookmark; if (exists) { bookmarkArguments["index"] = exists["index"]; bookmark = lazy.Bookmarks.update(bookmarkArguments); } else { bookmark = lazy.Bookmarks.insert(bookmarkArguments); } let result = await bookmark; if (bookmarkArguments["url"]) { let iconUrl = bookmarkArguments["iconUrl"]; if (!iconUrl) { iconUrl = 'fake-favicon-uri:' + bookmarkArguments["url"]; } let url = lazy.NetUtil.newURI(bookmarkArguments["url"]); let rIconUrl = lazy.NetUtil.newURI(iconUrl); if (bookmarkArguments["icon"]) { let icon = bookmarkArguments["icon"]; if (lazy.PlacesUtils.favicons.setFaviconForPage) { let iconDataUrl = lazy.NetUtil.newURI(icon); lazy.PlacesUtils.favicons.setFaviconForPage( url, rIconUrl, iconDataUrl ); } else { lazy.PlacesUtils.favicons.replaceFaviconDataFromDataURL( rIconUrl, icon ); let iconResult = lazy.PlacesUtils.favicons.setAndFetchFaviconForPage( url, rIconUrl, false, lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null, Services.scriptSecurityManager.getSystemPrincipal() ); } } else { if (lazy.PlacesUtils.favicons.setFaviconForPage) { } else if (lazy.PlacesUtils.favicons.setAndFetchFaviconForPage) { let iconResult = lazy.PlacesUtils.favicons.setAndFetchFaviconForPage( url, rIconUrl, true, lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null, Services.scriptSecurityManager.getSystemPrincipal() ); } } } return bookmark; })(arguments[0]); return bookmarkStatus; _JS_ $self->_context($old); return $self; } sub delete_bookmark { my ( $self, $bookmark ) = @_; my $guid = $bookmark->guid(); my $old = $self->_context('chrome'); $self->script( $self->_compress_script( $self->_bookmark_interface_preamble() . <<'_JS_'), args => [$guid] ); return lazy.Bookmarks.remove(arguments[0]); _JS_ $self->_context($old); return $self; } sub _generate_history_guid { my ($self) = @_; # from GenerateGUID in ./toolkit/components/places/Helpers.cpp my $guid = MIME::Base64::encode_base64( Crypt::URandom::urandom( _SHORT_GUID_BYTES() ) ); $guid =~ s/\//-/smxg; $guid =~ s/[+]/_/smxg; chomp $guid; return $guid; } sub import_bookmarks { my ( $self, $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; my $contents; my $result; while ( $result = $read_handle->read( my $buffer, _LOCAL_READ_BUFFER_SIZE() ) ) { $contents .= $buffer; } my $default_menu_title = q[menu]; defined $result or Firefox::Marionette::Exception->throw( "Failed to read from '$path':$EXTENDED_OS_ERROR"); my $quoted_header_regex = quotemeta <<'_HTML_'; _HTML_ $quoted_header_regex =~ s/\\\r?\n/\\s+/smxg; my $title_regex = qr/[<]TITLE[>]Bookmarks[<]\/TITLE[>]\s+/smx; my $header_regex = qr/[<]H1[>]Bookmarks(?:[ ]Menu)?[<]\/H1[>]\s+/smx; my $list_regex = qr/[<]DL[>][<]p[>]\s*/smx; if ( $contents =~ s/\A\s*$quoted_header_regex\s*//smx ) { $contents =~ s/\A\s*]+><\/meta>\s*//smx; $contents =~ s/\A$title_regex$header_regex$list_regex//smx; my %mapping = $self->_get_bookmark_mapping(); my $processing = 1; my $index = 0; my $json = { title => q[], index => $index++, $mapping{type} => Firefox::Marionette::Bookmark::FOLDER(), guid => Firefox::Marionette::Bookmark::ROOT(), children => [], }; my @folders; push @folders, $json; my $folder_name_regex = qr/(UNFILED_BOOKMARKS|PERSONAL_TOOLBAR)/smx; my $folder_regex = qr/\s*[<]DT[>]/smx . qr/[<]H3(?:[ ]ADD_DATE="(\d+)")?(?:[ ]LAST_MODIFIED="(\d+)")?/smx . qr/(?:[ ]${folder_name_regex}_FOLDER="true")?[>]/smx . qr/([^<]+)\s*<\/H3>/smx; my $bookmark_regex = qr/\s*[<]DT[>][<]A[ ]HREF="([^"]+)"[ ]/smx . qr/ADD_DATE="(\d+)"(?:[ ]LAST_MODIFIED="(\d+)")?/smx . qr/(?:[ ]ICON_URI="([^"]+)")?(?:[ ]ICON="([^"]+)")?/smx . qr/(?:[ ]SHORTCUTURL="([^"]+)")?(?:[ ]TAGS="([^"]+)")?[>]/smx . qr/([^<]+)[<]\/A[>]\s*/smx; while ($processing) { $processing = 0; if ( $contents =~ s/\A$folder_regex//smx ) { my ( $add_date, $last_modified, $type_of_folder, $text ) = ( $1, $2, $3, $4 ); $processing = 1; if ( !$type_of_folder ) { my $implied_menu_folder = { title => Encode::decode( 'UTF-8', $default_menu_title, 1 ), index => $index++, $mapping{type} => Firefox::Marionette::Bookmark::FOLDER(), guid => Firefox::Marionette::Bookmark::MENU(), children => [], }; push @{ $folders[-1]->{children} }, $implied_menu_folder; push @folders, $implied_menu_folder; } my $folder_name = $text; my $folder = { title => Encode::decode( 'UTF-8', $folder_name, 1 ), index => $index++, $mapping{type} => Firefox::Marionette::Bookmark::FOLDER(), ( $type_of_folder ? ( $type_of_folder eq 'PERSONAL_TOOLBAR' ? ( guid => Firefox::Marionette::Bookmark::TOOLBAR() ) : ( guid => Firefox::Marionette::Bookmark::UNFILED() ) ) : () ), $mapping{date_added} => $self->_fix_bookmark_date_from_html($add_date), ( $last_modified ? ( $mapping{last_modified} => $self->_fix_bookmark_date_from_html( $last_modified), ) : () ), children => [], }; push @{ $folders[-1]->{children} }, $folder; push @folders, $folder; } if ( $contents =~ s/\A\s*[<]DL[>][<]p[>]\s*//smx ) { $processing = 1; } if ( $contents =~ s/\A$bookmark_regex//smx ) { my ( $link, $add_date, $last_modified, $icon_uri, $icon, $keyword, $tags, $text ) = ( $1, $2, $3, $4, $5, $6, $7, $8 ); my $link_name = $text; $processing = 1; my $bookmark = { title => Encode::decode( 'UTF-8', $link_name, 1 ), uri => $link, $mapping{icon_url} => $icon_uri, icon => $icon, index => $index++, $mapping{type} => Firefox::Marionette::Bookmark::BOOKMARK(), $mapping{date_added} => $self->_fix_bookmark_date_from_html($add_date), $mapping{last_modified} => $self->_fix_bookmark_date_from_html($last_modified), tags => Encode::decode( 'UTF-8', $tags, 1 ), keyword => Encode::decode( 'UTF-8', $keyword, 1 ), }; push @{ $folders[-1]->{children} }, $bookmark; } if ( $contents =~ s/\A\s*[<]HR[>]\s*//smx ) { $processing = 1; my $separator = { index => $index++, $mapping{type} => Firefox::Marionette::Bookmark::SEPARATOR(), }; push @{ $folders[-1]->{children} }, $separator; } if ( $contents =~ s/\A\s*[<]\/DL[>](?:[<]p[>])?\s*//smx ) { $processing = 1; pop @folders; } } if ($contents) { Firefox::Marionette::Exception->throw( 'Unrecognised format for bookmark import'); } $json = $self->_find_existing_guids($json); $self->_import_bookmark_json_children( {}, $json ); } else { my $json = JSON::decode_json($contents); $self->_import_bookmark_json_children( {}, $json ); } return $self; } sub _fix_bookmark_date_from_html { my ( $self, $date ) = @_; if ($date) { $date .= '000000'; } return $date; } sub _assign_guid_for_existing_child { my ( $self, $result, $child, %mapping ) = @_; if ( $result->type() == $child->{ $mapping{type} } ) { if ( $result->type() == Firefox::Marionette::Bookmark::FOLDER() ) { if ( $result->title() eq $child->{title} ) { $child->{guid} = $result->guid(); } } elsif ( $result->type() == Firefox::Marionette::Bookmark::BOOKMARK() ) { if ( $result->url() eq $child->{uri} ) { $child->{guid} = $result->guid(); } } else { # Firefox::Marionette::Bookmark::SEPARATOR() $child->{guid} = $result->guid(); } } return; } sub _find_existing_guids { my ( $self, $json ) = @_; foreach my $child ( @{ $json->{children} } ) { if ( $child->{guid} ) { } else { my $index = 0; my %mapping = $self->_get_bookmark_mapping(); while ( ( !$child->{guid} ) && ( defined $index ) ) { my $result = $self->_get_bookmark( { parent_guid => $json->{guid}, index => $index } ); if ( defined $result ) { $self->_assign_guid_for_existing_child( $result, $child, %mapping ); $index += 1; } else { $child->{guid} = $self->_generate_history_guid(); $index = undef; } } } if ( $child->{children} ) { $self->_find_existing_guids($child); } } return $json; } sub _import_bookmark_json_children { my ( $self, $grand_parent, $parent ) = @_; my $date_added = $parent->{dateAdded}; if ( defined $date_added ) { $date_added =~ s/\d{6}$//smx; $date_added += 0; } my $last_modified = $parent->{lastModified}; if ( defined $last_modified ) { $last_modified =~ s/\d{6}$//smx; $last_modified += 0; } my $bookmark = Firefox::Marionette::Bookmark->new( guid => $parent->{guid}, parent_guid => $grand_parent->{guid}, title => $parent->{title}, url => $parent->{uri}, date_added => $date_added, last_modified => $last_modified, type => $parent->{typeCode}, icon_url => $parent->{iconUri}, icon => $parent->{icon}, ( $parent->{tags} ? ( tags => [ split /\s*,\s*/smx, $parent->{tags} ] ) : () ), keyword => $parent->{keyword}, ); if ( ( $bookmark->guid() ) && ( !$self->_get_bookmark( $bookmark->guid() ) ) ) { $self->add_bookmark($bookmark); } elsif ( $bookmark->url() ) { my $found; foreach my $existing ( $self->bookmarks( $bookmark->url()->as_string() ) ) { if ( $existing->url() eq $bookmark->url() ) { $found = 1; } } if ( !$found ) { $self->add_bookmark($bookmark); } } foreach my $child ( @{ $parent->{children} } ) { $self->_import_bookmark_json_children( $parent, $child ); } 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() ) ) { print {$write_handle} $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"); close $write_handle or Firefox::Marionette::Exception->throw( "Failed to close '$write_path':$EXTENDED_OS_ERROR"); } close $read_handle or Firefox::Marionette::Exception->throw( "Failed to close '$path':$EXTENDED_OS_ERROR"); } } return; } sub webauthn_authenticator { my ($self) = @_; return $self->{$webauthn_default_authenticator_key_name}; } sub add_webauthn_authenticator { my ( $self, %parameters ) = @_; if ( !defined $parameters{protocol} ) { $parameters{protocol} = 'ctap2'; } if ( !defined $parameters{transport} ) { $parameters{transport} = 'internal'; } foreach my $key ( qw( has_resident_key is_user_consenting is_user_verified has_user_verification ) ) { $parameters{$key} = $self->_translate_to_json_boolean( $self->_default_to_true( $parameters{$key} ) ); } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebAuthn:AddVirtualAuthenticator'), { protocol => $parameters{protocol}, transport => $parameters{transport}, hasResidentKey => $parameters{has_resident_key}, hasUserVerification => $parameters{has_user_verification}, isUserConsenting => $parameters{is_user_consenting}, isUserVerified => $parameters{is_user_verified}, } ] ); my $response = $self->_get_response($message_id); return Firefox::Marionette::WebAuthn::Authenticator->new( id => $self->_response_result_value($response), %parameters ); } sub _default_to_true { my ( $self, $boolean ) = @_; if ( !defined $boolean ) { $boolean = 1; } return $boolean; } sub webauthn_set_user_verified { my ( $self, $boolean, $parameter_authenticator ) = @_; my $authenticator = $self->_get_webauthn_authenticator( authenticator => $parameter_authenticator ); $boolean = $self->_translate_to_json_boolean( $self->_default_to_true($boolean) ); my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebAuthn:SetUserVerified'), { authenticatorId => $authenticator->id(), isUserVerified => $boolean, } ] ); my $response = $self->_get_response($message_id); return $self; } sub delete_webauthn_authenticator { my ( $self, $parameter_authenticator ) = @_; my $authenticator = $self->_get_webauthn_authenticator( authenticator => $parameter_authenticator ); my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebAuthn:RemoveVirtualAuthenticator'), { authenticatorId => $authenticator->id(), } ] ); my $response = $self->_get_response($message_id); if ( ( $self->webauthn_authenticator() ) && ( $self->webauthn_authenticator()->id() == $authenticator->id() ) ) { delete $self->{$webauthn_default_authenticator_key_name}; } return $self; } sub add_webauthn_credential { my ( $self, %parameters ) = @_; foreach my $key ( qw( is_resident ) ) { $parameters{$key} = $self->_translate_to_json_boolean( $self->_default_to_true( $parameters{$key} ) ); } if ( !defined $parameters{id} ) { my $credential_id = MIME::Base64::encode_base64url( Crypt::URandom::urandom( _CREDENTIAL_ID_LENGTH() ) ); $parameters{id} = $credential_id; } if ( defined $parameters{user} ) { $parameters{user} = MIME::Base64::encode_base64url( $parameters{user} ); } if ( !defined $parameters{private_key} ) { $parameters{private_key} = {}; } if ( ref $parameters{private_key} eq 'HASH' ) { my $script = <<'_JS_'; let privateKeyArguments = {}; if (arguments[0]["name"]) { privateKeyArguments["name"] = arguments[0]["name"]; } else { privateKeyArguments["name"] = "RSA-PSS"; } if ((privateKeyArguments["name"] == "RSA-PSS") || (privateKeyArguments["name"] == "RSASSA-PKCS1-v1_5")) { privateKeyArguments["modulusLength"] = arguments[0]["size"] || 8192; privateKeyArguments["publicExponent"] = new Uint8Array([1, 0, 1]); privateKeyArguments["hash"] = arguments[0]["hash"] || "SHA-512"; } else if ((privateKeyArguments["name"] == "ECDSA") || (privateKeyArguments["name"] == "ECDH")) { privateKeyArguments["namedCurve"] = arguments[0]["curve"] || "P-384"; } let privateKey = (async function() { let keyPair = await window.crypto.subtle.generateKey( privateKeyArguments, true, ["sign"] ); let exportedKey = await window.crypto.subtle.exportKey("pkcs8", keyPair.privateKey); let CHUNK_SZ = 0x8000; let c = []; let array = new Uint8Array(exportedKey); for (let i = 0; i < array.length; i += CHUNK_SZ) { c.push(String.fromCharCode.apply(null, array.subarray(i, i + CHUNK_SZ))); } let b64Key = window.btoa(c.join("")); let urlSafeKey = b64Key.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); return urlSafeKey; })(); return privateKey; _JS_ my $old = $self->_context('chrome'); $parameters{private_key} = $self->script( $self->_compress_script($script), args => [ $parameters{private_key} ] ); $self->_context($old); } if ( !defined $parameters{sign_count} ) { $parameters{sign_count} = 0; } my $authenticator = $self->_get_webauthn_authenticator(%parameters); my %credential_parameters = ( authenticatorId => $authenticator->id(), credentialId => $parameters{id}, isResidentCredential => $parameters{is_resident}, rpId => $parameters{host}, privateKey => $parameters{private_key}, signCount => $parameters{sign_count}, userHandle => $parameters{user}, ); my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebAuthn:AddCredential'), \%credential_parameters, ] ); if ( defined $credential_parameters{userHandle} ) { $credential_parameters{userHandle} = MIME::Base64::decode_base64url( $credential_parameters{userHandle} ); } my $response = $self->_get_response($message_id); return Firefox::Marionette::WebAuthn::Credential->new( %credential_parameters); } sub _get_webauthn_authenticator { my ( $self, %parameters ) = @_; my $authenticator; if ( $parameters{authenticator} ) { $authenticator = $parameters{authenticator}; } else { $authenticator = $self->webauthn_authenticator(); } return $authenticator; } sub webauthn_credentials { my ( $self, $parameter_authenticator ) = @_; my $authenticator = $self->_get_webauthn_authenticator( authenticator => $parameter_authenticator ); my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebAuthn:GetCredentials'), { authenticatorId => $authenticator->id(), } ] ); my $response = $self->_get_response($message_id); return map { Firefox::Marionette::WebAuthn::Credential->new( %{$_} ) } map { $self->_decode_credential_user_handle($_) } @{ $self->_response_result_value($response) }; } sub _decode_credential_user_handle { my ( $self, $credential ) = @_; if ( $credential->{userHandle} eq q[] ) { $credential->{userHandle} = undef; } else { $credential->{userHandle} = MIME::Base64::decode_base64url( $credential->{userHandle} ); } return $credential; } sub delete_webauthn_all_credentials { my ( $self, $parameter_authenticator ) = @_; my $authenticator = $self->_get_webauthn_authenticator( authenticator => $parameter_authenticator ); my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebAuthn:RemoveAllCredentials'), { authenticatorId => $authenticator->id(), } ] ); my $response = $self->_get_response($message_id); return $self; } sub delete_webauthn_credential { my ( $self, $credential, $parameter_authenticator ) = @_; my $authenticator = $self->_get_webauthn_authenticator( authenticator => $parameter_authenticator ); my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebAuthn:RemoveCredential'), { authenticatorId => $authenticator->id(), credentialId => $credential->id(), } ] ); my $response = $self->_get_response($message_id); return $self; } 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', 'last modified' => 'password_changed_time', created => 'creation_time', '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', ); my %time_mapping = ( 'last modified' => 1, 'created' => 1, ); 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} ]; if ( $time_mapping{$key} ) { if ( $parameters{ $mapping{$key} } =~ /^(\d{4})\-(\d{2})\-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/smx ) { my ( $year, $month, $day, $hour, $mins, $secs ) = ( $1, $2, $3, $4, $5, $6 ); my $time = Time::Local::timegm( $secs, $mins, $hour, $day, $month - 1, $year ); $parameters{ $mapping{$key} } = $time; } } } } 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_xml { my ( $class, $import_handle ) = @_; my $parser = XML::Parser->new(); my @parsed_pw_entries; my $current_pw_entry; my $key_regex_string = join q[|], qw( username url password uuid creationtime lastmodtime lastaccesstime ); my $key_name; $parser->setHandlers( Start => sub { my ( $p, $element, %attributes ) = @_; if ( $element eq 'pwentry' ) { $current_pw_entry = {}; $key_name = undef; } elsif ( $element =~ /^($key_regex_string)$/smx ) { $key_name = ($1); } else { $key_name = undef; } }, Char => sub { my ( $p, $string ) = @_; if ( defined $key_name ) { chomp $string; $current_pw_entry->{$key_name} .= $string; } }, End => sub { my ( $p, $element ) = @_; $key_name = undef; if ( $element eq 'pwentry' ) { push @parsed_pw_entries, $current_pw_entry; } }, ); $parser->parse($import_handle); my @logins; foreach my $pw_entry (@parsed_pw_entries) { my $login = {}; foreach my $key (qw(creationtime lastmodtime lastaccesstime)) { if ( ( defined $pw_entry->{$key} ) && ( $pw_entry->{$key} =~ /^(\d{4})\-(\d{2})\-(\d{2})T(\d{2}):(\d{2}):(\d{2})$/smx ) ) { my ( $year, $month, $day, $hour, $mins, $secs ) = ( $1, $2, $3, $4, $5, $6 ); my $time = Time::Local::timegm( $secs, $mins, $hour, $day, $month - 1, $year ); $pw_entry->{$key} = $time; } } my $host; if ( defined $pw_entry->{url} ) { my $url = URI::URL->new( $pw_entry->{url} ); $host = URI::URL->new( $url->scheme() . q[://] . $url->host_port() ) ->canonical()->as_string; } if ( ( $pw_entry->{username} ) && ($host) && ( $pw_entry->{password} ) ) { push @logins, Firefox::Marionette::Login->new( host => $host, user => $pw_entry->{username}, password => $pw_entry->{password}, guid => $pw_entry->{uuid}, creation_time => $pw_entry->{creationtime}, password_changed_time => $pw_entry->{lastmodtime}, last_used_time => $pw_entry->{lastaccesstime} ); } } return @logins; } 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_time => $item->{createdAt}, password_changed_time => $item->{updatedAt}, ); } } } 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'); my $javascript = <<"_JS_"; # xpcom/ds/nsIWritablePropertyBag2.idl let updateMeta = function(mLoginInfo, aMetaInfo) { let loginMetaInfo = Components.classes["\@mozilla.org/hash-property-bag;1"].createInstance(Components.interfaces.nsIWritablePropertyBag2); if ("guid" in aMetaInfo && aMetaInfo.guid !== null) { loginMetaInfo.setPropertyAsAUTF8String("guid", aMetaInfo.guid); } if ("creation_in_ms" in aMetaInfo && aMetaInfo.creation_in_ms !== null) { loginMetaInfo.setPropertyAsUint64("timeCreated", aMetaInfo.creation_in_ms); } if ("last_used_in_ms" in aMetaInfo && aMetaInfo.last_used_in_ms !== null) { loginMetaInfo.setPropertyAsUint64("timeLastUsed", aMetaInfo.last_used_in_ms); } if ("password_changed_in_ms" in aMetaInfo && aMetaInfo.password_changed_in_ms !== null) { loginMetaInfo.setPropertyAsUint64("timePasswordChanged", aMetaInfo.password_changed_in_ms); } if ("times_used" in aMetaInfo && aMetaInfo.times_used !== null) { loginMetaInfo.setPropertyAsUint64("timesUsed", aMetaInfo.times_used); } loginManager.modifyLogin(mLoginInfo, loginMetaInfo); }; _JS_ if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_AWAIT() ) ) { $javascript .= <<"_JS_"; if (loginManager.initializationPromise) { return (async function(aLoginInfo, metaInfo) { await loginManager.initializationPromise; if (loginManager.addLoginAsync) { let rLoginInfo = await loginManager.addLoginAsync(aLoginInfo); updateMeta(rLoginInfo, metaInfo); return rLoginInfo; } else { loginManager.addLogin(loginInfo); updateMeta(loginInfo, metaInfo); return loginInfo; } })(loginInfo, arguments[0]); } else { loginManager.addLogin(loginInfo); updateMeta(loginInfo, arguments[0]); return loginInfo; } _JS_ } else { $javascript .= <<"_JS_"; loginManager.addLogin(loginInfo); updateMeta(loginInfo, arguments[0]); return loginInfo; _JS_ } $self->script( $self->_compress_script( $self->_login_interface_preamble() . $self->_define_login_info_from_blessed_user( 'loginInfo', $login ) . $javascript ), args => [$login] ); $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 _untaint_binary { my ( $self, $binary, $remote_path_to_binary ) = @_; if ( defined $remote_path_to_binary ) { my $quoted_binary = quotemeta $binary; if ( $remote_path_to_binary =~ /^([[:alnum:]\-\/\\:()~]*$quoted_binary)$/smx ) { return $1; } } return; } 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' ) { $binary = $self->_execute_via_ssh( {}, 'cygpath', '-u', $binary ); chomp $binary; my ( $volume, $directories ) = File::Spec::Unix->splitpath($binary); $binary_directory = File::Spec::Unix->catdir( $volume, $directories ); } else { my $remote_path_to_binary = $self->_untaint_binary( $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) = @_; my $directory = $self->_binary_directory(); if ( my $update_directory = $self->_updates_directory_exists($directory) ) { my @entries; foreach my $entry ( $self->_directory_listing( { ignore_missing_directory => 1 }, $update_directory, 1 ) ) { if ( $entry =~ /^(\d{1,10})$/smx ) { push @entries, $1; } } my @sorted_entries = reverse sort { $a <=> $b } @entries; return shift @sorted_entries; } return; } sub _most_recent_updates_status_path { my ( $self, $index ) = @_; if ( defined( my $most_recent_updates_index = $self->_most_recent_updates_index() ) ) { if ( my $updates_directory = $self->_updates_directory_exists( $self->_binary_directory() ) ) { return $self->_catfile( $updates_directory, $most_recent_updates_index, 'update.status' ); } } 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( { ignore_exit_status => 1 }, $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 ( ( $self->_ssh() ) || ( $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 _displays { my ($self) = @_; # Retrieved and translated from https://en.wikipedia.org/wiki/List_of_common_resolutions my $displays = <<"_DISPLAYS_"; 0.26K1 Microvision 16 16 1:1 1:1 1:1 256 0.46K1 Timex Datalink USB[1][2] 42 11 42:11 1:1 5:9 462 1.02K1 PocketStation 32 32 1:1 1:1 1:1 1,024 1.2K3 Etch A Sketch Animator 40 30 4:3 4:3 1:1 1,200 1.34K1 Epson RC-20[3] 42 32 42:32 1:1 0.762 1,344 1.54K2 GameKing I (GM-218), VMU 48 32 3:2 3:2 1:1 1,536 2.4K2 Etch A Sketch Animator 2000 60 40 3:2 3:2 1:1 2,400 4.03K7:4 Nokia 3210 and many other early Nokia Phones 84 48 7:4 2:1 1.143 4,032 4.1K1 Hartung Game Master 64 64 1:1 1:1 1:1 4,096 4.61K1 Field Technology CxMP smart watch[2] 72 64 72:64 1:1 0.889 4,608 4.61K1 Montblanc e-Strap[4] 128 36 128:36 1:1 0.281 4,608 4.8K1 Epoch Game Pocket Computer 75 64 75:64 1:1 1:1.171875 4,800 0.01M3.75 Entex Adventure Vision 150 40 150:40 3.75 1:1 6,000 0.01M2 First graphing calculators: Casio fx-7000G, TI-81 96 64 3:2 3:2 1:1 6,144 0.01M2 Pok\x{E9}mon Mini 96 64 3:2 3:2 1:1 6,144 0.01M2 TRS-80 128 48 128:48 3:2 0.563 6,144 0.01M2 Early Nokia colour screen phones 96 65 96:65 3:2 1.016 6,240 0.01MA Ruputer 102 64 102:64 8:5 1.004 6,528 0.01M4 Sony Ericsson T68i, T300, T310 and other early colour screen phones 101 80 101:80 5:4 0.99 8,080 0.01M1 MetaWatch Strata & Frame watches 96 96 1:1 1:1 1:1 9,216 0.02M3.75 Atari Portfolio, TRS-80 Model 100 240 64 240:64 3.75 1:1 15,360 0.02MA Atari Lynx 160 102 160:102 8:5 1.02 16,320 0.02M1 Sony SmartWatch, Sifteo cubes, early color screen phones (square display) 128 128 1:1 1:1 1:1 16,384 QQVGA Quarter Quarter VGA 160 120 4:3 4:3 1:1 19,200 0.02M1.111 Nintendo Game Boy (GB), Game Boy Color (GBC); Sega Game Gear (GG) 160 144 160:144 10:9 1:1 23,040 0.02M0.857 Pebble E-Paper Watch 144 168 144:168 6:7 1:1 24,192 0.02M1.053 Neo Geo Pocket Color 160 152 160:152 20:19 1:1 24,320 0.03M1 Palm LoRes 160 160 1:1 1:1 1:1 25,600 0.03M3 Apple II HiRes (6 color) and Apple IIe Double HiRes (16 color), grouping subpixels 140 192 140:192 4:3 1.828 26,880 0.03M3 VIC-II multicolor, IBM PCjr 16-color, Amstrad CPC 16-color 160 200 160:200 4:3 5:3 32,000 0.03M9 WonderSwan 224 144 14:9 14:9 1:1 32,256 0.04M13:11 Nokia Series 60 smartphones (Nokia 7650, plus First and Second Edition models only) 208 176 13:11 13:11 1:1 36,608 HQVGA Half QVGA: Nintendo Game Boy Advance 240 160 3:2 3:2 1:1 38,400 0.04M4 Older Java MIDP devices like Sony Ericsson K600 220 176 5:4 5:4 1:1 38,720 0.04M3 Acorn BBC 20 column modes 160 256 160:256 4:3 2.133 40,960 0.04M1 Nokia 5500 Sport, Nokia 6230i, Nokia 8800 208 208 1:1 1:1 1:1 43,264 0.05M3 TMS9918 modes 1 (e.g. TI-99/4A) and 2, ZX Spectrum, MSX, Sega Master System, Nintendo DS (each screen) 256 192 4:3 4:3 1:1 49,152 0.05M3 Apple II HiRes (1 bit per pixel) 280 192 280:192 4:3 0.914 53,760 0.05M3 MSX2 256 212 256:212 4:3 1.104 54,272 0.06M1 Samsung Gear Fit 432 128 432:128 1:1 0.296 55,296 0.06M3 Nintendo Entertainment System, Super Nintendo Entertainment System, Sega Mega Drive 256 224 256:224 4:3 7:6 57,344 0.06M1 Apple iPod Nano 6G 240 240 1:1 1:1 1:1 57,600 0.06M3 Sony PlayStation (e.g. Rockman Complete Works) 256 240 256:240 4:3 5:4 61,440 0.06M6 Atari 400/800 PAL 320 192 5:3 5:3 1:1 61,440 0.06M5:3 Atari 400/800 NTSC 320 192 5:3 50:35 6:7 61,440 Color Graphics Adapter (CGA) CGA 4-color, ATM 16 color, Atari ST 16 color, Commodore 64 VIC-II Hires, Amiga OCS NTSC Lowres, Apple IIGS LoRes, MCGA, Amstrad CPC 4-color 320 200 8:5 4:3 0.833 64,000 0.07M1 Elektronika BK 256 256 1:1 1:1 1:1 65,536 0.07M3 Sinclair QL 256 256 1:1 4:3 4:3 65,536 0.07M2 UIQ 2.x based smartphones 320 208 320:208 3:2 0.975 66,560 0.07M2 Sega Mega Drive, Sega Nomad, Neo Geo AES 320 224 10:7 3:2 1.05 71,680 QVGA Quarter VGA: Apple iPod Nano 3G, Sony PlayStation, Nintendo 64, Nintendo 3DS (lower screen) 320 240 4:3 4:3 1:1 76,800 0.08M4 Acorn BBC 40 column modes, Amiga OCS PAL Lowres 320 256 5:4 5:4 1:1 81,920 0.09M3 Capcom CP System (CPS, CPS2, CPS3) arcade system boards 384 224 384:224 4:3 0.778 86,016 0.09M3 Sony PlayStation (e.g. X-Men vs. Street Fighter) 368 240 368:240 4:3 0.869 88,320 0.09M9 Apple iPod Nano 5G 376 240 376:240 14:9 0.993 90,240 0.09M0.8 Apple Watch 38mm 272 340 272:340 4:5 1:1 92,480 WQVGA Wide QVGA: Common on Windows Mobile 6 handsets 400 240 5:3 5:3 1:1 96,000 0.1M3 Timex Sinclair 2068, Timex Computer 2048 512 192 512:192 4:3 0.5 98,304 0.1M3 IGS PolyGame Master arcade system board 448 224 2:1 4:3 0.667 100,352 0.1M1 Palm (PDA) HiRes, Samsung Galaxy Gear 320 320 1:1 1:1 1:1 102,400 WQVGA Wide QVGA: Apple iPod Nano 7G 432 240 9:5 9:5 1:1 103,680 0.11M3 Apple IIe Double Hires (1 bit per pixel)[5] 560 192 560:192 4:3 0.457 107,520 0.11M2 TurboExpress 400 270 400:270 3:2 1.013 108,000 0.11M3 MSX2 512 212 512:212 4:3 0.552 108,544 0.11M3 Common Intermediate Format 384 288 4:3 4:3 1:1 110,592 WQVGA* Variant used commonly for portable DVD players, digital photo frames, GPS receivers and devices such as the Kenwood DNX-5120 and Glospace SGK-70; often marketed as "16:9" 480 234 480:234 16:9 0.866 112,320 qSVGA Quarter SVGA: Selectable in some PC shooters 400 300 4:3 4:3 1:1 120,000 0.12M3 Teletext and Viewdata 40x25 character screens (PAL non-interlaced) 480 250 480:250 4:3 0.694 120,000 0.12M0.8 Apple Watch 42mm 312 390 312:390 4:5 1:1 121,680 0.12M3 Sony PlayStation (e.g. Tekken and Tekken 2) 512 240 512:240 4:3 0.625 122,880 0.13M3 Amiga OCS NTSC Lowres interlaced 320 400 320:400 4:3 5:3 128,000 Color Graphics Adapter (CGA) Atari ST 4 color, ATM, CGA mono, Amiga OCS NTSC Hires, Apple IIGS HiRes, Nokia Series 80 smartphones, Amstrad CPC 2-color 640 200 640:200 4:3 0.417 128,000 0.13M9 Sony PlayStation Portable, Zune HD, Neo Geo X 480 272 480:272 16:9 1.007 130,560 0.13M2:1 Elektronika BK, Polyplay 512 256 2:1 2:1 1:1 131,072 0.13M3 Sinclair QL 512 256 2:1 4:3 0.667 131,072 0.15M13:11 Nokia Series 60 smartphones (E60, E70, N80, N90) 416 352 13:11 13:11 1:1 146,432 HVGA Palm Tungsten T3, Apple iPhone, HTC Dream, Palm (PDA) HiRES+ 480 320 3:2 3:2 1:1 153,600 HVGA Handheld PC 640 240 640:240 8:3 1:1 153,600 0.15M3 Sony PlayStation 640 240 640:240 4:3 0.5 153,600 0.16M3 Acorn BBC 80 column modes, Amiga OCS PAL Hires 640 256 640:256 4:3 0.533 163,840 0.18M2 Black & white Macintosh (9") 512 342 512:342 3:2 1.002 175,104 0.18M3 Sony PlayStation (e.g. Tekken 3) (interlaced) 368 480 368:480 4:3 1.739 176,640 0.19M3 Sega Model 1 (e.g. Virtua Fighter) and Model 2 (e.g. Daytona USA) arcade system boards 496 384 496:384 4:3 1.032 190,464 0.19M6 Nintendo 3DS (upper screen in 3D mode: 2x 400 x 240, one for each eye) 800 240 800:240 5:3 0.5 192,000 0.2M3 Macintosh LC (12")/Color Classic (also selectable in many PC shooters) 512 384 4:3 4:3 1:1 196,608 0.2M2:1 Nokia Series 90 smartphones (7700, 7710) 640 320 2:1 2:1 1:1 204,800 EGA Enhanced Graphics Adapter 640 350 640:350 4:3 0.729 224,000 0.23M9 nHD, used by Nokia 5800, Nokia 5530, Nokia X6, Nokia N97, Nokia N8[6] 640 360 16:9 16:9 1:1 230,400 0.24M3 Teletext and Viewdata 40x25 character screens (PAL interlaced) 480 500 480:500 4:3 1.399 240,000 0.25M3 Namco System 12 arcade system board (e.g. Soulcalibur, Tekken 3, Tekken Tag Tournament) (interlaced) 512 480 512:480 4:3 5:4 245,760 0.25M3 HGC 720 348 720:348 4:3 0.644 250,560 0.25M3 MDA 720 350 720:350 4:3 0.648 252,000 0.26M3 Atari ST mono, Amiga OCS NTSC Hires interlaced 640 400 8:5 4:3 0.833 256,000 0.26M3 Apple Lisa 720 364 720:364 4:3 0.674 262,080 0.28M2.273 Nokia E90 Communicator 800 352 800:352 25:11 1:1 281,600 0.29M4 Some older monitors 600 480 5:4 5:4 1:1 288,000 VGA Video Graphics Array:MCGA (in monochome), Sun-1 color, Sony PlayStation (e.g. Tobal No.1 and Ehrgeiz), Nintendo 64 (e.g. various Expansion Pak enhanced games), 6th Generation Consoles, Nintendo Wii 640 480 4:3 4:3 1:1 307,200 0.33M3 Amiga OCS PAL Hires interlaced 640 512 5:4 4:3 1.066 327,680 WVGA Wide VGA 768 480 8:5 8:5 1:1 368,640 WGA Wide VGA: List of mobile phones with WVGA display 800 480 5:3 5:3 1:1 384,000 W-PAL Wide PAL 848 480 848:480 16:9 1.006 407,040 FWVGA List of mobile phones with FWVGA display 854 480 854:480 16:9 0.999 409,920 SVGA Super VGA 800 600 4:3 4:3 1:1 480,000 qHD Quarter FHD: AACS ICT, HRHD, Motorola Atrix 4G, Sony XEL-1[7][unreliable source?] 960 540 16:9 16:9 1:1 518,400 0.52M3 Apple Macintosh Half Megapixel[8] 832 624 4:3 4:3 1:1 519,168 0.52M9 PlayStation Vita (PSV) 960 544 960:544 16:9 1.007 522,240 0.59M9 PAL 16:9 1024 576 16:9 16:9 1:1 589,824 DVGA Double VGA: Apple iPhone 4S,[9][unreliable source?][10] 4th Generation iPod Touch[11] 960 640 3:2 3:2 1:1 614,400 WSVGA Wide SVGA: 10" netbooks 1024 600 1024:600 16:9 1.041 614,400 0.66MA Close to WSVGA 1024 640 8:5 8:5 1:1 655,360 0.69M3 Panasonic DVCPRO100 for 50/60 Hz over 720p - SMPTE Resolution 960 720 4:3 4:3 1:1 691,200 0.73M9 Apple iPhone 5, iPhone 5S, iPhone 5C, iPhone SE (1st) 1136 640 1136:640 16:9 1.001 727,040 0.73M9 Occasional Chromebook resolution with 96 DPI; see HP Chromebook 14A G5. 1138 640 16:9 16:9 0.999 728,320 XGA Extended Graphics Array:Common on 14"/15" TFTs and the Apple iPad 1024 768 4:3 4:3 1:1 786,432 0.82M3 Sun-1 monochrome 1024 800 32:25 4:3 1.041 819,200 0.83MA Supported by some GPUs, monitors, and games 1152 720 8:5 8:5 1:1 829,440 0.88M2 Apple PowerBook G4 (original Titanium version) 1152 768 3:2 3:2 1:1 884,736 WXGA-H Wide XGA:Minimum, 720p HDTV 1280 720 16:9 16:9 1:1 921,600 0.93M3 NeXT MegaPixel Display 1120 832 1120:832 4:3 0.99 931,840 WXGA Wide XGA:Average, BrightView 1280 768 5:3 5:3 1:1 983,040 XGA+ Apple XGA[note 2] 1152 864 4:3 4:3 1:1 995,328 1M9 Apple iPhone 6, iPhone 6S, iPhone 7, iPhone 8, iPhone SE (2nd) 1334 750 1334:750 16:9 0.999 1,000,500 WXGA Wide XGA:Maximum 1280 800 8:5 8:5 1:1 1,024,000 1.04M32:25 Sun-2 Prime Monochrome or Color Video, also common in Sun-3 and Sun-4 workstations 1152 900 32:25 32:25 1:1 1,036,800 1.05M1:1 Network Computing Devices 1024 1024 1:1 1:1 1:1 1,048,576 1.05M9 Standardized HDTV 720p/1080i displays or "HD ready", used in most cheaper notebooks 1366 768 1366:768 16:9 0.999 1,049,088 1.09M2 Apple PowerBook G4 1280 854 1280:854 3:2 1.001 1,093,120 SXGA- Super XGA "Minus" 1280 960 4:3 4:3 1:1 1,228,800 1.23M2.083 Sony VAIO P series 1600 768 1600:768 25:12 1:1 1,228,800 1.3M0.9 HTC Vive (per eye) 1080 1200 1080:1200 9:10 1:1 1,296,000 WSXGA Wide SXGA 1440 900 8:5 8:5 1:1 1,296,000 WXGA+ Wide XGA+ 1440 900 8:5 8:5 1:1 1,296,000 SXGA Super XGA 1280 1024 5:4 5:4 1:1 1,310,720 1.39M2 Apple PowerBook G4 1440 960 3:2 3:2 1:1 1,382,400 HD+ 900p 1600 900 16:9 16:9 1:1 1,440,000 SXGA+ Super XGA Plus, Lenovo Thinkpad X61 Tablet 1400 1050 4:3 4:3 1:1 1,470,000 1.47M5 Similar to A4 paper format (~123 dpi for A4 size) 1440 1024 1440:1024 7:5 0.996 1,474,560 1.56M3 HDV 1080i 1440 1080 4:3 4:3 1:1 1,555,200 1.64M10 SGI 1600SW 1600 1024 25:16 25:16 1:1 1,638,400 WSXGA+ Wide SXGA+ 1680 1050 8:5 8:5 1:1 1,764,000 1.78M9 Available in some monitors 1776 1000 1776:1000 16:9 1.001 1,776,000 UXGA Ultra XGA:Lenovo Thinkpad T60 1600 1200 4:3 4:3 1:1 1,920,000 2.05M4 Sun3 Hi-res monochrome 1600 1280 5:4 5:4 1:1 2,048,000 FHD Full HD:1080 HDTV (1080i, 1080p) 1920 1080 16:9 16:9 1:1 2,073,600 2.07M1 Windows Mixed Reality headsets (per eye) 1440 1440 1:1 1:1 1:1 2,073,600 DCI 2K DCI 2K 2048 1080 2048:1080 1.90:1 1.002 2,211,840 WUXGA Wide UXGA 1920 1200 8:5 8:5 1:1 2,304,000 QWXGA Quad WXGA, 2K 2048 1152 16:9 16:9 1:1 2,359,296 2.41M3 Supported by some GPUs, monitors, and games 1792 1344 4:3 4:3 1:1 2,408,448 FHD+ Full HD Plus:Microsoft Surface 3 1920 1280 3:2 3:2 1:1 2,457,600 2.46M2.10:1 Samsung Galaxy S10e, Xiaomi Mi A2 Lite, Huawei P20 Lite 2280 1080 2.10:1 2.10:1 1:1 2,462,400 2.53M2.167 Samsung Galaxy A8s, Xiaomi Redmi Note 7, Honor Play 2340 1080 19.5:9 19.5:9 1:1 2,527,200 2.58M3 Supported by some GPUs, monitors, and games 1856 1392 4:3 4:3 1:1 2,583,552 2.59M09? Samsung Galaxy A70, Samsung Galaxy S21/+, Xiaomi Redmi Note 9S, default for many 3200x1440 phones[12] 2400 1080 20:9 20:9 1:1 2,592,000 2.59M4 Supported by some GPUs, monitors, and games 1800 1440 5:4 5:4 1:1 2,592,000 CWSXGA NEC CRV43,[13] Ostendo CRVD,[14] Alienware Curved Display[15][16] 2880 900 2880:900 16:5 1:1 2,592,000 2.59M9 HTC Vive, Oculus Rift (both eyes) 2160 1200 9:5 9:5 1:1 2,592,000 2.62MA Supported by some GPUs, monitors, and games 2048 1280 8:5 8:5 1:1 2,621,440 TXGA Tesselar XGA 1920 1400 1920:1400 7:5 1.021 2,688,000 2.72M1A Motorola One Vision, Motorola One Action and Sony Xperia 10 IV 2520 1080 21:9 21:9 1:1 2,721,600 2.74M2.165 Apple iPhone X, iPhone XS and iPhone 11 Pro 2436 1125 2436:1125 2.165 1:1 2,740,500 2.74M1AD Avielo Optix SuperWide 235 projector[17] 2538 1080 2.35:1 2.35:1 1.017 2,741,040 2.76M3 Supported by some GPUs, monitors, and games 1920 1440 4:3 4:3 1:1 2,764,800 UW-FHD UltraWide FHD:Cinema TV from Philips and Vizio, Dell UltraSharp U2913WM, ASUS MX299Q, NEC EA294WMi, Philips 298X4QJAB, LG 29EA93, AOC Q2963PM 2560 1080 21:9 21:9 1:1 2,764,800 3.11M2 Microsoft Surface Pro 3 2160 1440 3:2 3:2 1:1 3,110,400 QXGA Quad XGA:iPad (3rd Generation), iPad Mini (2nd Generation) 2048 1536 4:3 4:3 1:1 3,145,728 3.32MA Maximum resolution of the Sony GDM-FW900, Hewlett Packard A7217A and the Retina Display MacBook 2304 1440 8:5 8:5 1:1 3,317,760 3.39MA Surface Laptop 2256 1504 3:2 8:5 1.067 3,393,024 WQHD Wide Quad HD:Dell UltraSharp U2711, Dell XPS One 27, Apple iMac 2560 1440 16:9 16:9 1:1 3,686,400 3.98M3 Supported by some displays and graphics cards[18][unreliable source?][19] 2304 1728 4:3 4:3 1:1 3,981,312 WQXGA Wide QXGA:Apple Cinema HD 30, Apple 13" MacBook Pro Retina Display, Dell Ultrasharp U3011, Dell 3007WFP, Dell 3008WFP, Gateway XHD3000, Samsung 305T, HP LP3065, HP ZR30W, Nexus 10 2560 1600 8:5 8:5 1:1 4,096,000 4.15M2:1 LG G6, LG V30, Pixel 2 XL, HTC U11+, Windows Mixed Reality headsets (both eyes) 2880 1440 2:1 2:1 1:1 4,147,200 Infinity Display Samsung Galaxy S8, S8+, S9, S9+, Note 8 2960 1440 18.5:9 18.5:9 1:1 4,262,400 4.35M2 Chromebook Pixel 2560 1700 3:2 3:2 1:1 4,352,000 4.61M1.422 Pixel C 2560 1800 64:45 64:45 1:1 4,608,000 4.67MA Lenovo Thinkpad W541 2880 1620 16:9 8:5 0.9 4,665,600 4.92M3 Max. CRT resolution, supported by the Viewsonic P225f and some graphics cards 2560 1920 4:3 4:3 1:1 4,915,200 Ultra-Wide QHD LG, Samsung, Acer, HP and Dell UltraWide monitors 3440 1440 21:9 21:9 1:1 4,953,600 4.99M2 Microsoft Surface Pro 4 2736 1824 3:2 3:2 1:1 4,990,464 5.18MA Apple 15" MacBook Pro Retina Display 2880 1800 8:5 8:5 1:1 5,184,000 QSXGA Quad SXGA 2560 2048 5:4 5:4 1:1 5,242,880 5.6M3 iPad Pro 12.9" 2732 2048 4:3 4:3 0.999 5,595,136 WQXGA+ Wide QXGA+:HP Envy TouchSmart 14, Fujitsu Lifebook UH90/L, Lenovo Yoga 2 Pro 3200 1800 16:9 16:9 1:1 5,760,000 QSXGA+ Quad SXGA+ 2800 2100 4:3 4:3 1:1 5,880,000 5.9MA Apple 16" MacBook Pro Retina Display 3072 1920 8:5 8:5 1:1 5,898,240 3K Microsoft Surface Book, Huawei MateBook X Pro[20] 3000 2000 3:2 3:2 1:1 6,000,000 UW4K Ultra-Wide 4K 3840 1600 2.35:1 21:9 0.988 6,144,000 WQSXGA Wide QSXGA 3200 2048 25:16 25:16 1:1 6,553,600 7M2 Microsoft Surface Book 2 15" 3240 2160 3:2 3:2 1:1 6,998,400 DWQHD Dual Wide Quad HD: Philips 499P9H, Dell U4919DW, Samsung C49RG94SSU 5120 1440 32:9 32:9 1:1 7,372,800 QUXGA Quad UXGA 3200 2400 4:3 4:3 1:1 7,680,000 4K UHD-1 4K Ultra HD 1:2160p, 4000-lines UHDTV (4K UHD) 3840 2160 16:9 16:9 1:1 8,294,400 DCI 4K DCI 4K 4096 2160 1.90:1 1.90:1 1.002 8,847,360 WQUXGA Wide QUXGA:IBM T221 3840 2400 8:5 8:5 1:1 9,216,000 9.44M9 LG Ultrafine 21.5, Apple 21.5" iMac 4K Retina Display 4096 2304 16:9 16:9 1:1 9,437,184 UW5K (WUHD) Ultra-Wide 5K:21:9 aspect ratio TVs 5120 2160 21:9 21:9 1:1 11,059,200 11.29M9 Apple 24" iMac 4.5K Retina Display 4480 2520 16:9 16:9 1:1 11,289,600 HXGA Hex XGA 4096 3072 4:3 4:3 1:1 12,582,912 13.5M2 Surface Studio 4500 3000 3:2 3:2 1:1 13,500,000 5K Dell UP2715K, LG Ultrafine 27, Apple 27" iMac 5K Retina Display 5120 2880 16:9 16:9 1:1 14,745,600 WHXGA Wide HXGA 5120 3200 8:5 8:5 1:1 16,384,000 HSXGA Hex SXGA 5120 4096 5:4 5:4 1:1 20,971,520 6K Apple 32" Pro Display XDR[21] 6K Retina Display 6016 3384 16:9 16:9 1:1 20,358,144 WHSXGA Wide HSXGA 6400 4096 25:16 25:16 1:1 26,214,400 HUXGA Hex UXGA 6400 4800 4:3 4:3 1:1 30,720,000 - 6480 3240 2:1 2:1 1:1 20,995,200 8K UHD-2 8K Ultra HD 2:4320p, 8000-lines UHDTV (8K UHD) 7680 4320 16:9 16:9 1:1 33,177,600 WHUXGA Wide HUXGA 7680 4800 8:5 8:5 1:1 36,864,000 8K Full Format DCI 8K 8192 4320 1.90:1 1.90:1 1.002 35,389,440 - 8192 4608 16:9 16:9 1:1 37,748,736 UW10K Ultra-Wide 10K 10240 4320 21:9 21:9 1:1 44,236,800 8K Fulldome 8K Fulldome 8192 8192 1:1 1:1 1:1 67,108,864 16K 16K 15360 8640 16:9 16:9 1:1 132,710,400 _DISPLAYS_ my @displays; my $csv = Text::CSV_XS->new( { auto_diag => 2, sep_char => "\t", binary => 1, allow_loose_quotes => 1 } ); foreach my $line ( split /\r?\n/smx, $displays ) { $csv->parse($line); my @fields = $csv->fields(); my $index = 0; push @displays, Firefox::Marionette::Display->new( designation => $fields[ $index++ ], usage => $fields[ $index++ ], width => $fields[ $index++ ], height => $fields[ $index++ ], sar => $fields[ $index++ ], dar => $fields[ $index++ ], par => $fields[ $index++ ] ); } @displays = sort { $a->total() < $b->total() } @displays; return @displays; } my $cached_per_display_key_name = '_cached_per_display'; sub _check_for_min_max_sizes { my ($self) = @_; if ( $self->{$cached_per_display_key_name} ) { } else { my ( $current_width, $current_height ) = @{ $self->script( $self->_compress_script( q[return [window.outerWidth, window.outerHeight]]) ) }; $self->maximise(); my ( $maximum_width, $maximum_height ) = @{ $self->script( $self->_compress_script( q[return [window.outerWidth, window.outerHeight]]) ) }; if ( $current_width > $maximum_width ) { $current_width = $maximum_width; } if ( $current_height > $maximum_height ) { $current_height = $maximum_height; } $self->_resize( 0, 0 ); # getting minimum size my ( $minimum_width, $minimum_height ) = @{ $self->script( $self->_compress_script( q[return [window.outerWidth, window.outerHeight]]) ) }; $self->{$cached_per_display_key_name} = { maximum => { width => $maximum_width, height => $maximum_height }, minimum => { width => $minimum_width, height => $minimum_height } }; if ( ( $current_width == $minimum_width ) && ( $current_height == $minimum_height ) ) { } else { $self->resize( $current_width, $current_height ); } } return; } sub displays { my ( $self, $filter ) = @_; my $csv = Text::CSV_XS->new( { auto_diag => 2, sep_char => "\t", binary => 1, allow_loose_quotes => 1 } ); my @displays; foreach my $display ( $self->_displays() ) { if ( ( defined $filter ) && ( $display->usage() !~ /$filter/smx ) ) { next; } push @displays, $display; } return @displays; } sub _resize { my ( $self, $width, $height ) = @_; my $old = $self->_context('chrome'); # https://developer.mozilla.org/en-US/docs/Web/API/Window/resizeTo # https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event my $result = $self->script( $self->_compress_script( <<"_JS_" let expectedWidth = arguments[0]; let expectedHeight = arguments[1]; if ((window.outerWidth == expectedWidth) && (window.outerHeight == expectedHeight)) { return true; } else if ((window.screen.availWidth < expectedWidth) || (window.screen.availHeight < expectedHeight)) { return false; } else { let windowResized = function(argumentWidth, argumentHeight) { return new Promise((resolve) => { let waitForResize = function(event) { window.removeEventListener('resize', waitForResize); if ((window.outerWidth == argumentWidth) && (window.outerHeight == argumentHeight)) { resolve(true); } else { resolve(false); } }; window.addEventListener('resize', waitForResize); window.resizeTo(expectedWidth, expectedHeight); }) }; let result = (async function() { let awaitResult = await windowResized(expectedWidth, expectedHeight); return awaitResult; })(); return result; } _JS_ ), args => [ $width, $height ] ); $self->_context($old); return $result; } sub resize { my ( $self, $width, $height ) = @_; $self->_check_for_min_max_sizes(); my $return_value; if ( $self->{$cached_per_display_key_name}->{maximum}->{height} < $height ) { $return_value = 0; } if ( $self->{$cached_per_display_key_name}->{maximum}->{width} < $width ) { $return_value = 0; } if ( $self->{$cached_per_display_key_name}->{minimum}->{height} > $height ) { $return_value = 0; } if ( $self->{$cached_per_display_key_name}->{minimum}->{width} > $width ) { $return_value = 0; } if ( defined $return_value ) { return $return_value; } else { return $self->_resize( $width, $height ) ? $self : undef; } } sub percentage_visible { my ( $self, $element ) = @_; my $percentage = $self->script( $self->_compress_script( <<"_JS_" let selectedElement = arguments[0]; let position = selectedElement.getBoundingClientRect(); let computedStyle = window.getComputedStyle(selectedElement); let totalVisible = 0; let totalPixels = 0; let visibleAtPoint = function(x,y) { let elementsAtPoint = document.elementsFromPoint(x, y); let visiblePoint = false; let foundAnotherVisibleElement = false; for (let i = 0; i < elementsAtPoint.length; i++) { let computedStyle = window.getComputedStyle(elementsAtPoint[i]); if ((computedStyle.visibility === 'hidden') || (computedStyle.visibility === 'collapse') || (computedStyle.display === 'none')) { if (elementsAtPoint[i].isEqualNode(selectedElement)) { visiblePoint = false; break; } } else { if (elementsAtPoint[i].isEqualNode(selectedElement)) { if (foundAnotherVisibleElement) { visiblePoint = false; } else { visiblePoint = true; break; } } else { foundAnotherVisibleElement = true; } } } return visiblePoint; }; for (let x = parseInt(position.left); x < parseInt(position.left + position.width); x++) { for (let y = parseInt(position.top); y < parseInt(position.top + position.height); y++) { let result = visibleAtPoint(x,y); if (result === false) { totalPixels = totalPixels + 1; } else if (result == true) { totalVisible = totalVisible + 1; totalPixels = totalPixels + 1; } } } if (totalPixels > 0) { return (totalVisible / totalPixels) * 100; } else { return 0; } _JS_ ), args => [$element] ); return $percentage; } 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( $self->_prefs_interface_preamble() . <<'_JS_') ); let disabledForTesting = branch.getBoolPref("app.update.disabledForTesting"); branch.setBoolPref("app.update.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"] = 'PENDING_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("app.update.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 is_trusted { my ( $self, $certificate ) = @_; my $db_key = $certificate->db_key(); chomp $db_key; my $encoded_db_key = URI::Escape::uri_escape($db_key); my $old = $self->_context('chrome'); my $trusted = $self->script( $self->_compress_script( $self->_certificate_interface_preamble() . <<'_JS_'), args => [$encoded_db_key] ); let certificate = certificateDatabase.findCertByDBKey(decodeURIComponent(arguments[0]), {}); if (certificateDatabase.isCertTrusted(certificate, Components.interfaces.nsIX509Cert.CA_CERT, Components.interfaces.nsIX509CertDB.TRUSTED_SSL)) { return true; } else { return false; } _JS_ $self->_context($old); return $trusted ? 1 : 0; } 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 eq 'ARRAY' ) { 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 _setup_shortcut_proxy { my ( $self, $proxy_parameter, $capabilities ) = @_; my $firefox_proxy; if ( ref $proxy_parameter eq 'ARRAY' ) { $firefox_proxy = Firefox::Marionette::Proxy->new( pac => Firefox::Marionette::Proxy->get_inline_pac( @{$proxy_parameter} ) ); } elsif ( $proxy_parameter->isa('Firefox::Marionette::Proxy') ) { $firefox_proxy = $proxy_parameter; } else { my $proxy_uri = URI->new($proxy_parameter); if ( $proxy_uri->scheme() eq 'https' ) { $firefox_proxy = Firefox::Marionette::Proxy->new( tls => $proxy_uri->host_port() ); } elsif ( $proxy_uri =~ /^socks([45])?:\/\/([^\/]+)/smx ) { my ( $protocol_version, $host_port ) = ( $1, $2 ); $firefox_proxy = Firefox::Marionette::Proxy->new( socks_protocol => $protocol_version, socks => $host_port ); } else { $firefox_proxy = Firefox::Marionette::Proxy->new( host => $proxy_uri->host_port() ); } } $capabilities->{proxy} = $firefox_proxy; return $capabilities; } 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); if ( my $proxy_parameter = delete $parameters{proxy} ) { if ( !$parameters{capabilities} ) { $parameters{capabilities} = Firefox::Marionette::Capabilities->new(); } $parameters{capabilities} = $self->_setup_shortcut_proxy( $proxy_parameter, $parameters{capabilities} ); } ( $session_id, $capabilities ) = $self->_initial_socket_setup( $socket, $parameters{capabilities} ); foreach my $certificate (@certificates) { $self->add_certificate( string => $certificate, trust => _DEFAULT_CERT_TRUST() ); } if ( $parameters{bookmarks} ) { $self->import_bookmarks( $parameters{bookmarks} ); } } 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 _install_extension { my ( $self, $module, $name ) = @_; $self->_build_local_extension_directory(); my $path = File::Spec->catfile( $self->{_local_extension_directory}, $name ); 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; print {$handle} MIME::Base64::decode_base64( $module->as_string() ) or Firefox::Marionette::Exception->throw( "Failed to write to '$path':$EXTENDED_OS_ERROR"); close $handle or Firefox::Marionette::Exception->throw( "Failed to close '$path':$EXTENDED_OS_ERROR"); return $self->install( $path, 1 ); } sub _install_extension_by_handle { my ( $self, $zip, $name ) = @_; $self->_build_local_extension_directory(); my $path = File::Spec->catfile( $self->{_local_extension_directory}, $name ); unlink $path or ( $OS_ERROR == POSIX::ENOENT() ) or Firefox::Marionette::Exception->throw( "Failed to unlink '$path':$EXTENDED_OS_ERROR"); 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; $zip->writeToFileHandle( $handle, 1 ) == Archive::Zip::AZ_OK() or Firefox::Marionette::Exception->throw( "Failed to write to '$path':$EXTENDED_OS_ERROR"); close $handle or Firefox::Marionette::Exception->throw( "Failed to close '$path':$EXTENDED_OS_ERROR"); return $self->install( $path, 1 ); } sub _post_launch_checks_and_setup { my ( $self, $timeouts ) = @_; $self->_write_local_proxy( $self->_ssh() ); if ( defined $timeouts ) { $self->timeouts($timeouts); } if ( $self->{stealth} ) { my $old_user_agent = $self->agent(); my $zip = Firefox::Marionette::Extension::Stealth->new(); $self->{stealth_extension} = $self->_install_extension_by_handle( $zip, 'stealth-0.0.1.xpi' ); $self->script( $self->_compress_script( Firefox::Marionette::Extension::Stealth->user_agent_contents() ) ); } if ( $self->{_har} ) { $self->_install_extension( 'Firefox::Marionette::Extension::HarExportTrigger', 'har_export_trigger-0.6.1-an+fx.xpi' ); } if ( $self->{force_webauthn} ) { $self->{$webauthn_default_authenticator_key_name} = $self->add_webauthn_authenticator(); } elsif ( defined $self->{force_webauthn} ) { } elsif ( $self->_is_webauthn_okay() ) { $self->{$webauthn_default_authenticator_key_name} = $self->add_webauthn_authenticator(); } if ( my $geo = delete $self->{geo} ) { $self->_setup_geo($geo); } if ( defined $self->{trackable} ) { $self->_setup_trackable( delete $self->{trackable} ); } 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 har { my ($self) = @_; my $context = $self->_context('content'); if ( $self->{_har} ) { while ( !$self->script( 'if (window.HAR && window.HAR.triggerExport) { return 1 }') ) { sleep 1; } } my $log = $self->script(<<'_JS_'); return (async function() { return await window.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} ) || ( $self->{stealth} ) ) { } 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 ( !$self->_visible() ) { Carp::carp('Unable to launch firefox with -headless option'); } $self->{visible} = 1; } elsif ( $self->_visible() ) { } 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 _restart_profile_directory { my ($self) = @_; my $profile_directory = $self->{_profile_directory}; 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 ); } return $profile_directory; } sub _get_remote_profile_directory { my ( $self, $profile_name ) = @_; my $profile_directory; if ( ( $self->_remote_uname() eq 'cygwin' ) || ( $self->_remote_uname() eq 'MSWin32' ) ) { my $appdata_directory = $self->_get_remote_environment_variable_via_ssh('APPDATA'); if ( $self->_remote_uname() eq 'cygwin' ) { $appdata_directory =~ s/\\/\//smxg; $appdata_directory = $self->_execute_via_ssh( {}, 'cygpath', '-u', $appdata_directory ); chomp $appdata_directory; } my $profile_ini_directory = $self->_remote_catfile( $appdata_directory, 'Mozilla', 'Firefox' ); my $profile_ini_path = $self->_remote_catfile( $profile_ini_directory, 'profiles.ini' ); my $handle = $self->_get_file_via_scp( {}, $profile_ini_path, 'profiles.ini file' ); my $config = Config::INI::Reader->read_handle($handle); $profile_directory = $self->_remote_catfile( Firefox::Marionette::Profile->directory( $profile_name, $config, $profile_ini_directory ) ); } else { my $profile_ini_directory; if ( $self->_remote_uname() eq 'darwin' ) { $profile_ini_directory = $self->_remote_catfile( 'Library', 'Application Support', 'Firefox' ); } else { $profile_ini_directory = $self->_remote_catfile( '.mozilla', 'firefox' ); } my $profile_ini_path = $self->_remote_catfile( $profile_ini_directory, 'profiles.ini' ); my $handle = $self->_get_file_via_scp( { ignore_exit_status => 1 }, $profile_ini_path, 'profiles.ini file' ) or Firefox::Marionette::Exception->throw( 'Failed to find the file ' . $self->_ssh_address() . ":$profile_ini_path which would indicate where the prefs.js file for the '$profile_name' is stored" ); my $config = Config::INI::Reader->read_handle($handle); $profile_directory = $self->_remote_catfile( Firefox::Marionette::Profile->directory( $profile_name, $config, $profile_ini_directory, $self->_ssh_address() ) ); } return $profile_directory; } 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'; } if ( ( defined $self->{debug} ) && ( $self->{debug} !~ /^[01]$/smx ) ) { push @arguments, '-MOZ_LOG=' . $self->{debug}; } push @arguments, $self->_check_addons(%parameters); push @arguments, $self->_check_visible(%parameters); push @arguments, $self->_profile_arguments(%parameters); if ( ( $self->{_har} ) || ( $parameters{devtools} ) ) { push @arguments, '--devtools'; } if ( $parameters{kiosk} ) { push @arguments, '--kiosk'; } return @arguments; } sub _profile_arguments { my ( $self, %parameters ) = @_; my @arguments; if ( $parameters{restart} ) { push @arguments, ( '-profile', $self->_restart_profile_directory(), '--no-remote', '--new-instance' ); } elsif ( $parameters{profile_name} ) { $self->{profile_name} = $parameters{profile_name}; if ( $self->_ssh() ) { $self->{_profile_directory} = $self->_get_remote_profile_directory( $parameters{profile_name} ); $self->{profile_path} = $self->_remote_catfile( $self->{_profile_directory}, 'prefs.js' ); } else { $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"); print {$handle} $mime_types_content or Firefox::Marionette::Exception->throw( "Failed to write to '$path':$EXTENDED_OS_ERROR"); close $handle or Firefox::Marionette::Exception->throw( "Failed to close '$path':$EXTENDED_OS_ERROR"); } push @arguments, ( '-profile', $profile_directory, '--no-remote', '--new-instance' ); } 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_webauthn_okay { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_WEBAUTHN() ) ) { return 1; } else { return 0; } } 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_modern_switch_window_okay { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_MODERN_SWITCH() ) ) { return 1; } else { return 0; } } sub _is_modern_go_okay { my ($self) = @_; if ( $self->_is_firefox_major_version_at_least( _MIN_VERSION_FOR_MODERN_GO() ) ) { 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 _setup_parameters_for_execute_via_ssh { my ( $self, $ssh ) = @_; my $parameters = {}; if ( !defined $ssh->{ssh_connections_to_host} ) { $parameters->{accept_new} = 1; } if ( !$ssh->{control_established} ) { $parameters->{master} = 1; } return $parameters; } sub execute { my ( $self, $binary, @arguments ) = @_; if ( my $ssh = $self->_ssh() ) { my $parameters = $self->_setup_parameters_for_execute_via_ssh($ssh); 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 ); $error = Symbol::gensym(); my $pid; eval { $pid = IPC::Open3::open3( $writer, $reader, $error, $binary, @arguments ); } or do { chomp $EVAL_ERROR; Firefox::Marionette::Exception->throw( "Failed to execute '$binary':$EVAL_ERROR"); }; $writer->autoflush(1); $reader->autoflush(1); $error->autoflush(1); my ( $result, $output ); while ( $result = sysread $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" ); while ( $result = sysread $error, my $buffer, _READ_LENGTH_OF_OPEN3_OUTPUT() ) { } defined $result or Firefox::Marionette::Exception->throw( q[Failed to read STDERR from '] . ( join q[ ], $binary, @arguments ) . "':$EXTENDED_OS_ERROR" ); close $writer or Firefox::Marionette::Exception->throw( "Failed to close STDIN for $binary:$EXTENDED_OS_ERROR"); close $reader or Firefox::Marionette::Exception->throw( "Failed to close STDOUT for $binary:$EXTENDED_OS_ERROR"); close $error or Firefox::Marionette::Exception->throw( "Failed to close STDERR for $binary:$EXTENDED_OS_ERROR"); waitpid $pid, 0; if ( $CHILD_ERROR == 0 ) { } else { Firefox::Marionette::Exception->throw( q[Failed to execute '] . ( join q[ ], $binary, @arguments ) . q[':] . $self->_error_message( $binary, $CHILD_ERROR ) ); } if ( defined $output ) { chomp $output; $output =~ s/\r$//smx; return $output; } } return; } sub _adb_serial { my ($self) = @_; my $adb = $self->_adb(); return join q[:], $adb->{host}, $adb->{port}; } sub _initialise_adb { my ($self) = @_; $self->execute( 'adb', 'connect', $self->_adb_serial() ); my $adb_regex = qr/package:(.*(firefox|fennec|fenix).*)/smx; my $binary = 'adb'; my @arguments = ( qw(-s), $self->_adb_serial(), 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"); close $handle or Firefox::Marionette::Exception->throw( "Failed to close '$path':$EXTENDED_OS_ERROR"); return $content; } sub _catfile { my ( $self, $base_directory, @parts ) = @_; my $path; if ( $self->_ssh() ) { $path = $self->_remote_catfile( $base_directory, @parts ); } else { $path = File::Spec->catfile( $base_directory, @parts ); } return $path; } sub _find_win32_active_update_xml { my ( $self, $update_directory ) = @_; foreach my $tainted_id ( $self->_directory_listing( {}, $update_directory, 1 ) ) { if ( $tainted_id =~ /^([A-F\d]{16})$/smx ) { my ($id) = ($1); my $sub_directory_path = $self->_catfile( $update_directory, $id ); if ( my $found = $self->_find_active_update_xml_in_directory( $sub_directory_path) ) { return $found; } } } return; } sub _find_active_update_xml_in_directory { my ( $self, $directory ) = @_; foreach my $entry ( $self->_directory_listing( {}, $directory, 1 ) ) { if ( $entry eq _ACTIVE_UPDATE_XML_FILE_NAME() ) { return $self->_catfile( $directory, _ACTIVE_UPDATE_XML_FILE_NAME() ); } } return; } sub _updates_directory_exists { my ( $self, $base_directory ) = @_; if ( !$self->{_cached_per_instance}->{_update_directory} ) { my $common_appdata_directory; if ( $self->_ssh() ) { if ( ( $self->_remote_uname() eq 'MSWin32' ) || ( $self->_remote_uname() eq 'cygwin' ) ) { $common_appdata_directory = $self->_get_remote_environment_variable_via_ssh( 'ALLUSERSPROFILE'); if ( $self->_remote_uname() eq 'cygwin' ) { $common_appdata_directory =~ s/\\/\//smxg; $common_appdata_directory = $self->_execute_via_ssh( {}, 'cygpath', '-u', $common_appdata_directory ); chomp $common_appdata_directory; } } } elsif ( $OSNAME eq 'MSWin32' ) { $common_appdata_directory = Win32::GetFolderPath( Win32::CSIDL_COMMON_APPDATA() ); } elsif ( $OSNAME ne 'cygwin' ) { $common_appdata_directory = $ENV{ALLUSERSPROFILE}; } if ( ($common_appdata_directory) && ( !$self->{_cached_per_instance}->{_mozilla_update_directory} ) ) { if ( my $sub_directory = $self->_get_microsoft_updates_sub_directory( $common_appdata_directory) ) { $base_directory = $sub_directory; $self->{_cached_per_instance}->{_mozilla_update_directory} = $base_directory; } } if ($base_directory) { foreach my $entry ( $self->_directory_listing( { ignore_missing_directory => 1 }, $base_directory, 1 ) ) { if ( $entry eq 'updates' ) { $self->{_cached_per_instance}->{_update_directory} = $self->_remote_catfile( $base_directory, 'updates' ); } } } } return $self->{_cached_per_instance}->{_update_directory}; } sub _get_microsoft_updates_sub_directory { my ( $self, $common_appdata_directory ) = @_; my $sub_directory; ENTRY: foreach my $entry ( $self->_directory_listing( { ignore_missing_directory => 1 }, $common_appdata_directory, 1 ) ) { if ( $entry =~ /^Mozilla/smx ) { my $first_updates_directory = $self->_catfile( $common_appdata_directory, $entry, 'updates' ); foreach my $entry ( $self->_directory_listing( { ignore_missing_directory => 1 }, $first_updates_directory, 1 ) ) { if ( $entry =~ /^[[:xdigit:]]{16}$/smx ) { if ( my $handle = $self->_open_handle_for_reading( $first_updates_directory, $entry, 'updates', '0', 'update.status' ) ) { $sub_directory = $self->_catfile( $first_updates_directory, $entry ); last ENTRY; } } } } } return $sub_directory; } sub _open_handle_for_reading { my ( $self, @path ) = @_; my $path = $self->_catfile(@path); if ( $self->_ssh() ) { if ( my $handle = $self->_get_file_via_scp( { ignore_exit_status => 1 }, $path, $path[-1], ' file' ) ) { return $handle; } } else { if ( my $handle = FileHandle->new( $path, Fcntl::O_RDONLY() ) ) { return $handle; } } return; } sub _active_update_xml_path { my ($self) = @_; my $path; my $directory = $self->_binary_directory(); if ( !defined $directory ) { } elsif ( $self->_ssh() ) { if ( ( $self->_remote_uname() eq 'MSWin32' ) || ( $self->_remote_uname() eq 'cygwin' ) ) { my $update_directory; if ( ( $update_directory = $self->_updates_directory_exists($directory) ) && ( my $found = $self->_find_win32_active_update_xml($update_directory) ) ) { $path = $found; } } else { if ( my $found = $self->_find_active_update_xml_in_directory($directory) ) { $path = $found; } } } else { if ( ( $OSNAME eq 'MSWin32' ) || ( $OSNAME eq 'cygwin' ) ) { my $update_directory; if ( ( $update_directory = $self->_updates_directory_exists($directory) ) && ( my $found = $self->_find_win32_active_update_xml($update_directory) ) ) { $path = $found; } } else { if ( my $found = $self->_find_active_update_xml_in_directory($directory) ) { $path = $found; } } } return $path; } sub _active_update_version { my ($self) = @_; my $active_update_version; if ( my $active_update_path = $self->_active_update_xml_path() ) { my $active_update_handle; if ( $self->_ssh() ) { $active_update_handle = $self->_get_file_via_scp( { ignore_exit_status => 1 }, $active_update_path, _ACTIVE_UPDATE_XML_FILE_NAME() ); } else { $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); } } return $active_update_version; } sub _application_ini_config { my ( $self, $binary ) = @_; my $application_ini_path; my $application_ini_handle; my $application_ini_name = 'application.ini'; if ( my $binary_directory = $self->_binary_directory() ) { if ( $self->_ssh() ) { if ( $self->_remote_uname() eq 'darwin' ) { $binary_directory =~ s/Contents\/MacOS$/Contents\/Resources/smx; } elsif ( $self->_remote_uname() eq 'cygwin' ) { $binary_directory = $self->_execute_via_ssh( {}, 'cygpath', '-u', $binary_directory ); chomp $binary_directory; } $application_ini_path = $self->_catfile( $binary_directory, $application_ini_name ); $application_ini_handle = $self->_get_file_via_scp( { ignore_exit_status => 1 }, $application_ini_path, $application_ini_name ); } else { if ( $OSNAME eq 'darwin' ) { $binary_directory =~ s/Contents\/MacOS$/Contents\/Resources/smx; } elsif ( $OSNAME eq 'cygwin' ) { if ( defined $binary_directory ) { $binary_directory = $self->execute( 'cygpath', '-u', $binary_directory ); } } $application_ini_path = File::Spec->catfile( $binary_directory, $application_ini_name ); $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); return $config; } return; } sub _search_for_version_in_application_ini { my ( $self, $binary ) = @_; my $active_update_version = $self->_active_update_version(); if ( my $config = $self->_application_ini_config($binary) ) { 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 _adb_package_name { my ($self) = @_; return $self->{adb_package_name}; } sub _adb_component_name { my ($self) = @_; return join q[.], $self->_adb_package_name, q[App]; } sub _get_version { my ($self) = @_; my $binary = $self->_binary(); $self->{binary} = $binary; my $version_string; my $version_regex = qr/(\d+)[.](\d+(?:\w\d+|\-\d+)?)(?:[.](\d+))*/smx; if ( $self->_adb() ) { my $package_name = $self->_initialise_adb(); my $dumpsys = $self->execute( 'adb', '-s', $self->_adb_serial(), '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) { $self->{adb_package_name} = $package_name; } else { Firefox::Marionette::Exception->throw( 'adb -s ' . $self->_adb_serial() . " 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 $waterfox_regex = qr/Waterfox(?:Limited)?[ ]Waterfox[ ]/smx; my $browser_regex = join q[|], qr/Mozilla[ ]Firefox[ ]/smx, qr/LibreWolf[ ]Firefox[ ]/smx, $waterfox_regex, 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' { my ( $browser_result, $major, $minor, $patch ) = ( $1, $2, $3, $4 ); if ( $browser_result eq 'Moonchild Productions Pale Moon ' ) { $self->{pale_moon} = 1; $self->{_initial_version}->{major} = _PALEMOON_VERSION_EQUIV(); } else { $self->{_initial_version}->{major} = $major; $self->{_initial_version}->{minor} = $minor; $self->{_initial_version}->{patch} = $patch; } if ( $browser_result =~ /^$waterfox_regex$/smx ) { $self->{waterfox} = 1; } } elsif ( defined $self->{_initial_version} ) { } elsif ( $version_string =~ /^Waterfox(?:Limited)?[ ]/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) = @_; if ( ( defined $self->{_firefox_pid} ) && ( $self->{_firefox_pid} =~ /^(\d+)/smx ) ) { return $1; } return; } 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 ( $self, @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 = $self->_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 ) = @_; my $binary = q["] . $self->_binary() . q["]; if ( $self->_visible() ) { if ( ( $self->_remote_uname() eq 'MSWin32' ) || ( $self->_remote_uname() eq 'darwin' ) || ( $self->_visible() eq 'local' ) || ( $self->_remote_uname() eq 'cygwin' ) ) { } else { @arguments = ( '-a', '-s', q["] . ( join q[ ], $self->_xvfb_common_arguments() ) . q["], $binary, @arguments, ); $binary = 'xvfb-run'; } } 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( graphical => 1, env => 1 ), $self->_ssh_address() ); my $process = $self->_start_win32_process( 'ssh', @ssh_arguments, $binary, @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( graphical => 1, env => 1 ), $self->_ssh_address(), $binary, @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_via_adb { my ( $self, @arguments ) = @_; my $binary = q[adb]; my $package_name = $self->_adb_package_name(); my $component_name = $self->_adb_component_name(); @arguments = ( ( qw(-s), $self->_adb_serial(), qw(shell am start -W -n), ( join q[/], $package_name, $component_name ), qw(--es), q[args -marionette] ), ); $self->execute( $binary, @arguments ); return; } 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->_adb() ) { $self->_launch_via_adb(@arguments); return; } if ( $self->_ssh() ) { $self->{_local_ssh_pid} = $self->_launch_via_ssh(@arguments); $self->_wait_for_any_background_update_status(); 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_any_background_update_status(); return; } sub _launch_win32 { my ( $self, @arguments ) = @_; my $binary = $self->_binary(); if ( $binary =~ /[.]pl$/smx ) { unshift @arguments, $binary; $binary = $EXECUTABLE_NAME; } 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->catfile( 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; } 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"); close $auth_handle 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->catfile( 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[.]; print {$source_handle} "add :$display_number $xauth_proto $mcookie\n" or Firefox::Marionette::Exception->throw( "Failed to write to temporary file:$EXTENDED_OS_ERROR"); 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 _xvfb_common_arguments { my ($self) = @_; 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 = ( '-screen' => '0', $width_height_depth, ); return @arguments; } 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 @arguments = ( '-displayfd' => fileno $display_no_handle, $self->_xvfb_common_arguments(), '-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', '/Applications/LibreWolf.app/Contents/MacOS/librewolf', ); } my %_known_win32_organisations = ( 'Mozilla Firefox' => 'Mozilla', 'Mozilla Firefox ESR' => 'Mozilla', 'Firefox Developer Edition' => 'Mozilla', Nightly => 'Mozilla', 'Waterfox' => 'WaterfoxLimited', '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+))?\0?/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; my $remote_uname = $self->_remote_uname(); if ( !defined $remote_uname ) { return; } elsif ( $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 $result = $self->_execute_via_ssh( { return_exit_status => 1 }, ( $self->_remote_uname() eq 'cygwin' ? ( '/bin/kill', '-W' ) : ('kill') ), '-0', $remote_pid ); if ( $result == 0 ) { $self->{last_remote_alive_status} = 1; } else { $self->{last_remote_alive_status} = 0; } return $self->{last_remote_alive_status}; } sub alive { my ($self) = @_; if ( $self->_adb() ) { my $parameters; my $binary = q[adb]; my @arguments = ( qw(-s), $self->_adb_serial(), qw(shell am stack list) ); my $handle = $self->_get_local_handle_for_generic_command_output( $parameters, $binary, @arguments ); my $quoted_package_name = quotemeta $self->_adb_package_name(); my $quoted_component_name = quotemeta $self->_adb_component_name(); my $found = 0; while ( my $line = <$handle> ) { if ( $line =~ /^[ ]+taskId=\d+:[ ]$quoted_package_name\/${quoted_component_name}[ ]+/smx ) { $found = 1; } } return $found; } 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 = $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 _network_connection_and_initial_read_from_marionette { my ( $self, $socket, $sock_addr ) = @_; my ( $port, $host ) = Socket::unpack_sockaddr_in($sock_addr); $host = Socket::inet_ntoa($host); my $connected; if ( connect $socket, $sock_addr ) { my $number_of_bytes = sysread $socket, $self->{_initial_octet_read_from_marionette_socket}, 1; if ($number_of_bytes) { $connected = 1; } elsif ( defined $number_of_bytes ) { sleep 1; } else { Firefox::Marionette::Exception->throw( "Failed to read from connection to $host on port $port:$EXTENDED_OS_ERROR" ); } } 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"); } return $connected; } 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() ) ) { if ( $self->_adb() ) { Firefox::Marionette::Exception->throw( 'TODO: Cannot connect to android yet. Patches welcome'); } $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 ); $connected = $self->_network_connection_and_initial_read_from_marionette( $socket, $sock_addr ); } $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 = qw(-2 -a -T); if ( ( $parameters{graphical} ) || ( $parameters{master} ) ) { if ( ( defined $self->_visible() ) && ( $self->_visible() eq 'local' ) ) { push @arguments, '-X'; } } 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 $template_prefix = 'firefox_marionette_local_'; if ( $self->{reconnect_index} ) { $template_prefix .= $self->{reconnect_index} . q[-]; } my $root_directory = File::Temp->newdir( CLEANUP => 0, TEMPLATE => File::Spec->catdir( File::Spec->tmpdir(), $template_prefix . 'X' x _NUMBER_OF_CHARS_IN_TEMPLATE() ) ) 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}; } print {$local_proxy_handle} JSON::encode_json($local_proxy) or Firefox::Marionette::Exception->throw( "Failed to write to $local_proxy_path:$EXTENDED_OS_ERROR"); close $local_proxy_handle 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() ) ) { if ( $self->_ssh() ) { $self->{_root_directory} = $self->_get_remote_root_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 _new_profile_path { my ($self) = @_; 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' ); } return $profile_path; } sub _setup_new_profile { my ( $self, $profile, %parameters ) = @_; $self->_setup_profile_directories($profile); $self->{profile_path} = $self->_new_profile_path(); if ($profile) { if ( !$profile->download_directory() ) { my $download_directory = $self->{_download_directory}; if ( $self->_ssh() ) { if ( $self->_remote_uname() eq 'cygwin' ) { $download_directory = $self->_execute_via_ssh( {}, 'cygpath', '-s', '-w', $download_directory ); chomp $download_directory; } } elsif ( $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; } $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( $self->{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"); print {$handle} $content or Firefox::Marionette::Exception->throw( "Failed to write to $path:$EXTENDED_OS_ERROR"); close $handle 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"; }; exit 1; } else { Firefox::Marionette::Exception->throw( "Failed to fork:$EXTENDED_OS_ERROR"); } return $handle; } sub _get_local_command_output { my ( $self, $parameters, $binary, @arguments ) = @_; local $SIG{PIPE} = 'IGNORE'; my $output; my $handle; if ( $OSNAME eq 'MSWin32' ) { my $shell_command = $self->_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 $parameters->{return_exit_status} or Firefox::Marionette::Exception->throw( "Failed to read from $binary " . ( join q[ ], @arguments ) . ":$EXTENDED_OS_ERROR" ); close $handle or $parameters->{ignore_exit_status} or $parameters->{return_exit_status} or Firefox::Marionette::Exception->throw( q[Command '] . ( join q[ ], $binary, @arguments ) . q[ did not complete successfully:] . $self->_error_message( $binary, $CHILD_ERROR ) ); if ( $parameters->{return_exit_status} ) { return $CHILD_ERROR; } else { 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->{force_scp_protocol} ) { push @arguments, qw(-O); } 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 ( $self->{ssh_via_host} ) { push @arguments, ( '-o' => 'ProxyJump=' . $self->{ssh_via_host} ); } 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 = $self->_quoting_for_cmd_exe( $binary, @arguments ); if ( $self->_execute_win32_process( $binary, @arguments ) ) { $result = 1; } else { $result = 0; } } else { local $SIG{PIPE} = 'IGNORE'; 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 ) { $result = 1; } elsif ( $parameters->{ignore_exit_status} ) { $result = 0; } 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 $result; } 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 ( ( $self->_remote_uname() eq 'MSWin32' ) && ( $remote_path !~ /^[[:alnum:]:\-+\\\/.]+$/smx ) ) { $remote_path = $self->_execute_via_ssh( {}, q[for %A in ("] . $remote_path . q[") do ] . q[@] . q[echo %~sA] ); chomp $remote_path; $remote_path =~ s/\r$//smx; } if ( $OSNAME eq 'MSWin32' ) { $remote_path = $self->_quoting_for_cmd_exe($remote_path); } else { if ( ( $self->_remote_uname() eq 'MSWin32' ) || ( $self->_remote_uname() eq 'cygwin' ) ) { $remote_path =~ s/\\/\//smxg; } } $remote_path =~ s/[ ]/\\ /smxg; my @arguments = ( $self->_scp_arguments(), $self->_ssh_address() . ":$remote_path", $local_path, ); if ( $self->_system( $parameters, '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() ) ) { print {$temp_handle} $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 = $self->_quoting_for_cmd_exe($remote_path); } $remote_path =~ s/[ ]/\\ /smxg; 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} ) { } elsif ( $self->_adb() ) { } 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( { ignore_exit_status => 1 }, $self->{profile_path}, 'profile path' ); } else { $handle = $self->_search_file_via_ssh( $self->{profile_path}, 'profile path', [ 'marionette', 'security', ] ); } my $port; if ( defined $handle ) { 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 ) = @_; $path =~ s/[ ]/\\ /smxg; 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"); print {$handle} $output 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"); 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; } } close $profile_handle or Firefox::Marionette::Exception->throw( "Failed to close '$self->{profile_path}':$EXTENDED_OS_ERROR"); } elsif (( $OSNAME eq 'MSWin32' ) && ( $EXTENDED_OS_ERROR == _WIN32_ERROR_SHARING_VIOLATION() ) ) { $port = 0; } } 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 _split_browser_version { my ($self) = @_; my ( $major, $minor, $patch ); my $browser_version = $self->browser_version(); if ( defined $browser_version ) { ( $major, $minor, $patch ) = split /[.]/smx, $browser_version; } return ( $major, $minor, $patch ); } sub _check_ftp_support_for_proxy_request { my ( $self, $proxy, $build ) = @_; if ( $proxy->ftp() ) { my ( $major, $minor, $patch ) = $self->_split_browser_version(); if ( ( defined $major ) && ( $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 ****' ); } } return $build; } 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(); } $build = $self->_check_ftp_support_for_proxy_request( $proxy, $build ); 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} = Firefox::Marionette::Proxy::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 ) = $self->_split_browser_version(); if ( $self->{waterfox} ) { } elsif ( ( defined $major ) && ( $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'; } if ( ( $key eq 'all' ) && ( $uri->scheme() eq 'https' ) ) { $build->{proxyType} = 'pac'; $build->{proxyAutoconfigUrl} = Firefox::Marionette::Proxy->get_inline_pac($uri); } elsif ( $value =~ /^socks(?:[45])?:\/\/(.*?(:\d+)?)$/smx ) { $build->{ $build_key . 'Proxy' } = $1; } else { $build->{ $build_key . 'Proxy' } = $uri->host_port(); } } } return $self->_convert_proxy_before_request($build); } sub _translate_to_json_boolean { my ( $self, $boolean ) = @_; $boolean = $boolean ? JSON::true() : JSON::false(); return $boolean; } 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', ); if ( $self->_is_firefox_major_version_at_least( _MAX_VERSION_NO_POINTER_ORIGIN() ) ) { delete $booleans{moz_use_non_spec_compliant_pointer_origin}; } foreach my $method ( sort { $a cmp $b } keys %booleans ) { if ( defined $capabilities->$method() ) { $actual->{ $booleans{$method} } = $self->_translate_to_json_boolean( $capabilities->$method() ); } } 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, $new ) = @_; 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}; } return; } sub _get_moz_headless { my ( $self, $headless, $parameters ) = @_; 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; } return $headless; } 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; $parameters->{'moz:headless'} = $self->_get_moz_headless( $headless, $parameters ); 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 => ( defined $parameters->{rotatable} and ${ $parameters->{rotatable} } ) ? 1 : 0, platform_version => $self->_platform_version_from_capabilities($parameters), 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 _platform_version_from_capabilities { my ( $self, $parameters ) = @_; return defined $parameters->{'moz:platformVersion'} ? $parameters->{'moz:platformVersion'} : $parameters->{platformVersion} , # https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/103#marionette } 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 => $self->_translate_to_json_boolean( $cookie->http_only() ), secure => $self->_translate_to_json_boolean( $cookie->secure() ), 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/\b\/\/.*$//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() ) { if ( ( $response->result()->{value} ) && ( $response->result()->{value}->{capabilities} ) ) { return $self->_create_capabilities( $response->result()->{value}->{capabilities} ); } else { 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 aria_label { # https://bugzilla.mozilla.org/show_bug.cgi?id=1585622 my ( $self, $element ) = @_; if ( !$self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { Firefox::Marionette::Exception->throw( 'get_aria_label method requires a Firefox::Marionette::Element parameter' ); } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetComputedLabel'), { id => $element->uuid() } ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response); } sub aria_role { # https://bugzilla.mozilla.org/show_bug.cgi?id=1585622 my ( $self, $element ) = @_; if ( !$self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { Firefox::Marionette::Exception->throw( 'get_aria_role method requires a Firefox::Marionette::Element parameter' ); } my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command('WebDriver:GetComputedRole'), { id => $element->uuid() } ] ); my $response = $self->_get_response($message_id); return $self->_response_result_value($response); } 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' ); next if ( $key eq 'scale' ); 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} = $self->_translate_to_json_boolean( $parameters{$key} ); } } 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); print {$handle} MIME::Base64::decode_base64($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"); return $handle; } } sub scroll { my ( $self, $element, $arguments ) = @_; if ( !$self->_is_marionette_object( $element, 'Firefox::Marionette::Element' ) ) { Firefox::Marionette::Exception->throw( 'scroll method requires a Firefox::Marionette::Element parameter'); } if ( defined $arguments ) { if ( ref $arguments ) { } else { $arguments = $self->_translate_to_json_boolean($arguments); } $self->script( 'arguments[0].scrollIntoView(arguments[1]);', args => [ $element, $arguments ] ); } else { $self->script( 'arguments[0].scrollIntoView();', args => [$element] ); } return $self; } 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} = $self->_translate_to_json_boolean( $extra{$key} ); } } $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; print {$handle} MIME::Base64::decode_base64($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"); 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 Firefox::Marionette::WebWindow->new( $self, Firefox::Marionette::WebWindow::IDENTIFIER() => $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 Firefox::Marionette::WebWindow->new( $self, Firefox::Marionette::WebWindow::IDENTIFIER() => $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 wheel { my ( $self, @parameters ) = @_; my %arguments; if ( $self->_is_marionette_object( $parameters[0], 'Firefox::Marionette::Element' ) ) { my $origin = shift @parameters; %arguments = $self->_calculate_xy_from_element( $origin, %arguments ); } while (@parameters) { my $key = shift @parameters; $arguments{$key} = shift @parameters; } foreach my $key (qw(x y duration deltaX deltaY)) { $arguments{$key} ||= 0; } return { type => 'scroll', %arguments }; } sub mouse_move { my ( $self, @parameters ) = @_; my %arguments; if ( $self->_is_marionette_object( $parameters[0], 'Firefox::Marionette::Element' ) ) { my $origin = shift @parameters; %arguments = $self->_calculate_xy_from_element( $origin, %arguments ); } while (@parameters) { my $key = shift @parameters; $arguments{$key} = shift @parameters; } return { type => 'pointerMove', pointerType => 'mouse', %arguments }; } sub _calculate_xy_from_element { my ( $self, $origin, %arguments ) = @_; 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; } return %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 %type_map = ( keyUp => 'key', keyDown => 'key', scroll => 'wheel', ); my %arguments; if ( ( $marionette_action->{type} eq 'keyUp' ) || ( $marionette_action->{type} eq 'keyDown' ) || ( $marionette_action->{type} eq 'scroll' ) ) { $type = $type_map{ $marionette_action->{type} }; } 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, pause and wheel 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 map { Firefox::Marionette::WebWindow->new( $self, Firefox::Marionette::WebWindow::IDENTIFIER(), $_ ) } @{ $response->result() }; } else { return map { Firefox::Marionette::WebWindow->new( $self, Firefox::Marionette::WebWindow::IDENTIFIER(), $_ ) } @{ $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 Firefox::Marionette::WebWindow->new( $self, Firefox::Marionette::WebWindow::IDENTIFIER() => $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 map { Firefox::Marionette::WebWindow->new( $self, Firefox::Marionette::WebWindow::IDENTIFIER(), $_ ) } @{ $response->result() }; } else { return map { Firefox::Marionette::WebWindow->new( $self, Firefox::Marionette::WebWindow::IDENTIFIER(), $_ ) } @{ $response->result()->{value} }; } } sub new_window { my ( $self, %parameters ) = @_; foreach my $key (qw(focus private)) { if ( defined $parameters{$key} ) { $parameters{$key} = $self->_translate_to_json_boolean( $parameters{$key} ); } } 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 Firefox::Marionette::WebWindow->new( $self, Firefox::Marionette::WebWindow::IDENTIFIER() => $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 ( Firefox::Marionette::WebWindow->new( $self, Firefox::Marionette::WebWindow::IDENTIFIER() => $self->_response_result_value($response) ) ); } else { return map { Firefox::Marionette::WebWindow->new( $self, Firefox::Marionette::WebWindow::IDENTIFIER() => $_ ) } @{ $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 ( Firefox::Marionette::WebWindow->new( $self, Firefox::Marionette::WebWindow::IDENTIFIER() => $response->result() ) ); } else { return map { Firefox::Marionette::WebWindow->new( $self, Firefox::Marionette::WebWindow::IDENTIFIER() => $_ ) } @{ $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 _determine_from { my ( $self, $from ) = @_; my $parameters = {}; if ( defined $from ) { if ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) { if ( ( defined $from ) && ( ref $from eq 'Firefox::Marionette::ShadowRoot' ) ) { $parameters->{shadowRoot} = $from->uuid(); } else { $parameters->{element} = $from->uuid(); } } else { $parameters->{ELEMENT} = $from->uuid(); } } return %{$parameters}; } sub _retry_find_response { my ( $self, $command, $parameters, $options ) = @_; my $message_id = $self->_new_message_id(); $self->_send_request( [ _COMMAND(), $message_id, $self->_command($command), $parameters ] ); return $self->_get_response( $message_id, { using => $parameters->{using}, value => $parameters->{value} }, $options ); } sub _get_and_retry_find_response { my ( $self, $value, $using, $from, $options ) = @_; my $want_array = delete $options->{want_array}; my $message_id = $self->_new_message_id(); my $parameters = { using => $using, value => $value, $self->_determine_from($from) }; my $command = $want_array ? 'WebDriver:FindElements' : 'WebDriver:FindElement'; if ( $parameters->{shadowRoot} ) { $command .= 'FromShadowRoot'; } $self->_send_request( [ _COMMAND(), $message_id, $self->_command($command), $parameters, ] ); my $response; eval { $response = $self->_get_response( $message_id, { using => $using, value => $value }, $options ); } or do { my $quoted_using = quotemeta $using; my $quoted_value = quotemeta $value; my $invalid_selector_re = qr/invalid[ ]selector:[ ]/smx . qr/Given[ ](?:$quoted_using)[ ]expression[ ]/smx . qr/["](?:$quoted_value)["][ ]is[ ]invalid:[ ]/smx; my $type_error_tag_re = qr/TypeError:[ ]/smx . qr/startNode[.]getElementsByTagName[ ]is[ ]not[ ]a[ ]function/smx; my $not_supported_re = qr/NotSupportedError:[ ]Operation[ ]is[ ]not[ ]supported/smx; my $type_error_class_re = qr/TypeError:[ ]/smx . qr/startNode[.]getElementsByClassName[ ]is[ ]not[ ]a[ ]function/smx; if ( $EVAL_ERROR =~ /^$invalid_selector_re$type_error_tag_re/smx ) { $parameters->{using} = 'css selector'; $response = $self->_retry_find_response( $command, $parameters, $options ); } elsif ( $EVAL_ERROR =~ /^$invalid_selector_re$not_supported_re/smx ) { $parameters->{using} = 'css selector'; $parameters->{value} = q{[name="} . $parameters->{value} . q["]; $response = $self->_retry_find_response( $command, $parameters, $options ); } elsif ( $EVAL_ERROR =~ /^$invalid_selector_re$type_error_class_re/smx ) { $parameters->{using} = 'css selector'; $parameters->{value} = q[.] . $parameters->{value}; $response = $self->_retry_find_response( $command, $parameters, $options ); } else { Carp::croak($EVAL_ERROR); } }; return $response; } sub _find { my ( $self, $value, $using, $from, $options ) = @_; $using ||= 'xpath'; $options->{want_array} = wantarray; my $response = $self->_get_and_retry_find_response( $value, $using, $from, $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 ) = @_; my $ssh_local_directory = $self->ssh_local_directory(); 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() ) { eval { if ( $self->_session_id() ) { $self->_quit_over_marionette($flags); delete $self->{session_id}; } $self->_terminate_xvfb(); 1; } or do { warn "Caught an exception while quitting:$EVAL_ERROR\n"; }; eval { if ( $self->_ssh() ) { $self->_cleanup_remote_filesystem(); $self->_terminate_master_control_via_ssh(); } $self->_cleanup_local_filesystem(); delete $self->{creation_pid}; } or do { warn "Caught an exception while cleaning up:$EVAL_ERROR\n"; }; $self->_terminate_process(); } else { $self->_terminate_process(); } if ( !$self->_reconnected() ) { if ($ssh_local_directory) { File::Path::rmtree( $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->_wait_for_firefox_to_exit(); } if ( defined $self->{_win32_firefox_process} ) { $self->{_win32_firefox_process}->Wait( Win32::Process::INFINITE() ); $self->_wait_for_firefox_to_exit(); } } elsif ( ( $OSNAME eq 'MSWin32' ) && ( !$self->_ssh() ) ) { $self->{_win32_firefox_process}->Wait( Win32::Process::INFINITE() ); $self->_wait_for_firefox_to_exit(); } else { if ( !close $socket ) { my $error = $EXTENDED_OS_ERROR; $self->_terminate_xvfb(); Firefox::Marionette::Exception->throw( "Failed to close socket to firefox:$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 ( kill 0, $self->_local_ssh_pid() ) { sleep 1; $self->_reap(); } } if ( $self->_firefox_pid() ) { while ( $self->_remote_process_running( $self->_firefox_pid() ) ) { sleep 1; } } } 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) = @_; if ( !$self->{_remote_root_directory} ) { $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 ) ); } $self->{_remote_root_directory} = $remote_root_directory; } return $self->{_remote_root_directory}; } sub uname { my ($self) = @_; if ( my $ssh = $self->_ssh() ) { return $self->_remote_uname(); } else { return $OSNAME; } } 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[%"]; } elsif (( $self->_remote_uname() ) && ( $self->_remote_uname() =~ /^(?:freebsd|dragonfly)$/smx ) ) { $command = 'echo ' . $name . q[=] . q[\\"] . 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 $parameters = { ignore_exit_status => 1 }; if ( $name eq 'DISPLAY' ) { $parameters->{graphical} = 1; } my $output = $self->_execute_via_ssh( $parameters, $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->_get_remote_root_directory() ) ) { my $binary = 'rm'; my @parameters = ('-Rf'); if ( $self->_remote_uname() eq 'MSWin32' ) { $binary = 'rmdir'; @parameters = ( '/S', '/Q' ); } my @remote_directories = ( $self->_get_remote_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, $binary, @parameters, $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 ( $self->_adb() ) { $self->execute( q[adb], qw(-s), $self->_adb_serial(), qw(shell am force-stop), $self->_adb_package_name() ); } else { 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 => $self->_translate_to_json_boolean($new) } ] ); 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} = $self->_translate_to_json_boolean( $parameters{$key} ); } } $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 Firefox::Marionette::WebFrame Firefox::Marionette::WebWindow ) ) { 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, $uri ) = @_; if ( defined $uri ) { my $old = $self->_context('chrome'); my $json = $self->script( $self->_compress_script( <<'_SCRIPT_'), args => [$uri] ); return (async function(url) { let response = await fetch(url, { method: "GET", mode: "cors", headers: { "Content-Type": "application/json" }, redirect: "follow", referrerPolicy: "no-referrer"}); if (response.ok) { return await response.json(); } else { throw new Error(url + " returned a " + response.status); } })(arguments[0]); _SCRIPT_ $self->_context($old); return $json; } else { 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'),
            {
                (
                    $self->_is_modern_switch_window_okay()
                    ? ()
                    : (
                        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",
                ( $self->_is_modern_go_okay() ? () : ( 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;
        }
        elsif ( FileHandle->new( $path, Fcntl::O_RDONLY() ) ) {
            ( $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
            }
        }
        else {
            Firefox::Marionette::Exception->throw(
                "Failed to find extension $path:$EXTENDED_OS_ERROR");
        }
        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 _addons_directory {
    my ($self) = @_;
    return $self->{_addons_directory};
}

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->{_root_directory}   = $self->_get_remote_root_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;
        my $addons_directory = $self->_addons_directory();
        $actual_path = $self->_remote_catfile( $addons_directory, $name );
        $self->_put_file_via_scp( $handle, $actual_path, 'addon ' . $name );
        if ( $self->_remote_uname() eq 'cygwin' ) {
            $addons_directory =
              $self->_execute_via_ssh( {}, 'cygpath', '-s', '-w',
                $addons_directory );
            chomp $addons_directory;
            $actual_path =
              File::Spec::Win32->catdir( $addons_directory, $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 => $self->_translate_to_json_boolean($temporary),
            }
        ]
    );
    my $response = $self->_get_response($message_id);
    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 _handle_socket_read_failure {
    my ($self) = @_;
    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);
    }
    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;
        my $octet;
        if ( $self->{_initial_octet_read_from_marionette_socket} ) {
            $octet = delete $self->{_initial_octet_read_from_marionette_socket};
            $number_of_bytes = length $octet;
        }
        else {
            $number_of_bytes = sysread $self->_socket(), $octet, 1;
        }
        if ( defined $number_of_bytes ) {
            $initial_buffer .= $octet;
        }
        else {
            $self->_handle_socket_read_failure();
        }
        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();
        }
    }
    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.63

=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('page-content')->find_id('metacpan_search-input')->type('Test::More');

    say "Height of page-content div is " . $firefox->find_class('page-content')->css('height');

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

    $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();

    $firefox->find_partial('Download')->click();

=head1 DESCRIPTION

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

=head1 CONSTANTS

=head2 BCD_PATH

returns the local path used for storing the brower compability data for the L method when the C parameter is supplied to the L method.  This database is built by the L binary.

=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_bookmark

accepts a L as a parameter and adds the specified bookmark to the Firefox places database.

    use Firefox::Marionette();

    my $bookmark = Firefox::Marionette::Bookmark->new(
                     url   => 'https://metacpan.org',
                     title => 'This is MetaCPAN!'
                             );
    my $firefox = Firefox::Marionette->new()->add_bookmark($bookmark);

This method returns L to aid in chaining methods.

=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 containing 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

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

    # or just directly

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

Note for HTTP Authentication, the L must perfectly match the correct L supplied by the server.

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 add_webauthn_authenticator

accepts a hash of the following keys;

=over 4

=item * has_resident_key - boolean value to indicate if the authenticator will support L

=item * has_user_verification - boolean value to determine if the L supports L.

=item * is_user_consenting - boolean value to determine the result of all L L, and by extension, any L performed on the L. If set to true, a L will always be granted. If set to false, it will not be granted.

=item * is_user_verified - boolean value to determine the result of L performed on the L. If set to true, L will always succeed. If set to false, it will fail.

=item * protocol - the L spoken by the authenticator.  This may be L, L or L.

=item * transport - the L simulated by the authenticator.  This may be L, L, L, L, L or L.

=back

It returns the newly created L.

    use Firefox::Marionette();
    use Crypt::URandom();

    my $user_name = MIME::Base64::encode_base64( Crypt::URandom::urandom( 10 ), q[] ) . q[@example.com];
    my $firefox = Firefox::Marionette->new( webauthn => 0 );
    my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() );
    $firefox->go('https://webauthn.io');
    $firefox->find_id('input-email')->type($user_name);
    $firefox->find_id('register-button')->click();
    $firefox->await(sub { sleep 1; $firefox->find_class('alert-success'); });
    $firefox->find_id('login-button')->click();
    $firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); });

=head2 add_webauthn_credential

accepts a hash of the following keys;

=over 4

=item * authenticator - contains the L that the credential will be added to.  If this parameter is not supplied, the credential will be added to the default authenticator, if one exists.

=item * host - contains the domain that this credential is to be used for.  In the language of L, this field is referred to as the L or L.

=item * id - contains the unique id for this credential, also known as the L.  If this is not supplied, one will be generated.

=item * is_resident - contains a boolean that if set to true, a L is created. If set to false, a L is created instead.

=item * private_key - either a L encoded private key encoded using L or a hash containing the following keys;

=over 8

=item * name - contains the name of the private key algorithm, such as "RSA-PSS" (the default), "RSASSA-PKCS1-v1_5", "ECDSA" or "ECDH".

=item * size - contains the modulus length of the private key.  This is only valid for "RSA-PSS" or "RSASSA-PKCS1-v1_5" private keys.

=item * hash - contains the name of the hash algorithm, such as "SHA-512" (the default).  This is only valid for "RSA-PSS" or "RSASSA-PKCS1-v1_5" private keys.

=item * curve - contains the name of the curve for the private key, such as "P-384" (the default).  This is only valid for "ECDSA" or "ECDH" private keys.

=back

=item * sign_count - contains the initial value for a L associated to the L.  It will default to 0 (zero).

=item * user - contains the L associated to the credential encoded using L.  This property is optional.

=back

It returns the newly created L.  If of course, the credential is just created, it probably won't be much good by itself.  However, you can use it to recreate a credential, so long as you know all the parameters.

    use Firefox::Marionette();
    use Crypt::URandom();

    my $user_name = MIME::Base64::encode_base64( Crypt::URandom::urandom( 10 ), q[] ) . q[@example.com];
    my $firefox = Firefox::Marionette->new();
    $firefox->go('https://webauthn.io');
    $firefox->find_id('input-email')->type($user_name);
    $firefox->find_id('register-button')->click();
    $firefox->await(sub { sleep 1; $firefox->find_class('alert-success'); });
    $firefox->find_id('login-button')->click();
    $firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); });
    foreach my $credential ($firefox->webauthn_credentials()) {
        $firefox->delete_webauthn_credential($credential);

# ... time passes ...

        $firefox->add_webauthn_credential(
                  id            => $credential->id(),
                  host          => $credential->host(),
                  user          => $credential->user(),
                  private_key   => $credential->private_key(),
                  is_resident   => $credential->is_resident(),
                  sign_count    => $credential->sign_count(),
                              );
    }
    $firefox->go('about:blank');
    $firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_COOKIES());
    $firefox->go('https://webauthn.io');
    $firefox->find_id('input-email')->type($user_name);
    $firefox->find_id('login-button')->click();
    $firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); });

=head2 addons

returns if pre-existing addons (extensions/themes) are allowed to run.  This will be true for Firefox versions less than 55, as L<-safe-mode|http://kb.mozillazine.org/Command_line_arguments#List_of_command_line_arguments_.28incomplete.29> cannot be automated.

=head2 agent

accepts an optional value for the L header and sets this using the profile preferences and inserting L into the current page. It returns the current value, such as 'Mozilla/5.0 ()  () '.  This value is retrieved with L.

This method can be used to set a user agent string like so;

    use Firefox::Marionette();
    use strict;

    # useragents.me should only be queried once a month or less.
    # these UA strings should be cached locally.

    my %user_agent_strings = map { $_->{ua} => $_->{pct} } @{$firefox->json("https://www.useragents.me/api")->{data}};
    my ($user_agent) = reverse sort { $user_agent_strings{$a} <=> $user_agent_strings{$b} } keys %user_agent_strings;

    my $firefox = Firefox::Marionette->new();
    $firefox->agent($user_agent); # agent is now the most popular agent from useragents.me

If the user agent string that is passed as a parameter looks like a L, L or L user agent string, then this method will also try and change other profile preferences to match the new agent string.  These parameters are;

=over 4

=item * general.appversion.override

=item * general.oscpu.override

=item * general.platform.override

=item * network.http.accept

=item * network.http.accept-encoding

=item * network.http.accept-encoding.secure

=item * privacy.donottrackheader.enabled

=back

In addition, this method will accept a hash of values as parameters as well.  When a hash is provided, this method will alter specific parts of the normal Firefox User Agent.  These hash parameters are;

=over 4

=item * os - The desired operating system, known values are "linux", "win32", "darwin", "freebsd", "netbsd", "openbsd" and "dragonfly"

=item * version - A specific version of firefox, such as 120.

=item * arch - A specific version of the architecture, such as "x86_64" or "aarch64" or "s390x".

=item * increment - A specific offset from the actual version of firefox, such as -5

=back

These parameters can be used to set a user agent string like so;

    use Firefox::Marionette();
    use strict;

    my $firefox = Firefox::Marionette->new();
    $firefox->agent(os => 'freebsd', version => 118);

    # user agent is now equal to
    # Mozilla/5.0 (X11; FreeBSD amd64; rv:109.0) Gecko/20100101 Firefox/118.0

    $firefox->agent(os => 'linux', arch => 's390x', version => 115);
    # user agent is now equal to
    # Mozilla/5.0 (X11; Linux s390x; rv:109.0) Gecko/20100101 Firefox/115.0

If the C parameter has supplied to the L method, it will also attempt to create known specific javascript functions to imitate the required browser.  If the database built by L is accessible, then it will also attempt to delete/provide dummy implementations for the corresponding L for the desired browser.  The following websites have been very useful in testing these ideas;

=over 4

=item * L

=item * L

=item * L

=item * L

=back

Importantly, this will break L for any website that relies on it.

See L a discussion of these types of techniques.  These changes are not foolproof, but it is interesting to see what can be done with modern browsers.  All this behaviour should be regarded as extremely experimental and subject to change.  Feedback welcome.

=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 arch

returns the architecture of the machine running firefox.  Should be something like 'x86_64' or 'arm'.  This is only intended for test suite support.

=head2 aria_label

accepts an L as the parameter.  It returns the L for the L.

=head2 aria_role

accepts an L as the parameter.  It returns the L for the L.

=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 from the HTML source code, 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('metacpan_search-input');
    !defined $element->attribute('value') or die "attribute is defined but did not exist in the html source!";
    $element->type('Test::More');
    !defined $element->attribute('value') or die "attribute has changed but only the property should have changed!";

=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->await(sub { $firefox->find_class('autocomplete-suggestion'); })->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 download

accepts a L and an optional timeout in seconds (the default is 5 minutes) as parameters and downloads the L in the background and returns a handle to the downloaded file.

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

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

    my $handle = $firefox->download('https://raw.githubusercontent.com/david-dick/firefox-marionette/master/t/data/keepassxs.csv');

    foreach my $line (<$handle>) {
      print $line;
    }

=head2 bookmarks

accepts either a scalar or a hash as a parameter.  The scalar may by the title of a bookmark or the L of the bookmark.  The hash may have the following keys;

=over 4

=item * title - The title of the bookmark.

=item * url - The url of the bookmark.

=back

returns a list of all L objects that match the supplied parameters (if any).

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

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

    foreach my $bookmark ($firefox->bookmarks(title => 'This is MetaCPAN!')) {
      say "Bookmark found";
    }

    # OR

    foreach my $bookmark ($firefox->bookmarks()) {
      say "Bookmark found with URL " . $bookmark->url();
    }

    # OR

    foreach my $bookmark ($firefox->bookmarks('https://metacpan.org')) {
      say "Bookmark found";
    }

=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->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();

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

=head2 cache_keys

returns the set of all cache keys from L.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();
    foreach my $key_name ($firefox->cache_keys()) {
      my $key_value = $firefox->check_cache_key($key_name);
      if (Firefox::Marionette::Cache->$key_name() != $key_value) {
        warn "This module this the value of $key_name is " . Firefox::Marionette::Cache->$key_name();
        warn "Firefox thinks the value of   $key_name is $key_value";
      }
    }

=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";
        print 'PEM Encoded Certificate ' . "\n" . $firefox->certificate_as_pem($certificate) . "\n";
    }

This method returns L to aid in chaining methods.

=head2 check_cache_key

accepts a L as a parameter.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();
    foreach my $key_name ($firefox->cache_keys()) {
      my $key_value = $firefox->check_cache_key($key_name);
      if (Firefox::Marionette::Cache->$key_name() != $key_value) {
        warn "This module this the value of $key_name is " . Firefox::Marionette::Cache->$key_name();
        warn "Firefox thinks the value of   $key_name is $key_value";
      }
    }

This method returns the L's actual value from firefox as a number.  This may differ from the current value of the key from L as these values have changed as firefox has evolved.

=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 a L 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 L 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 clear_cache

accepts a single flag parameter, which can be an ORed set of keys from L and clears the appropriate sections of the cache.  If no flags parameter is supplied, the default is L.  Note that this method, unlike L will actually delete all cookies for all hosts, not just the current webpage.

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

    my $firefox = Firefox::Marionette->new()->go('https://do.lots.of.evil/')->clear_cache(); # default clear all

    $firefox->go('https://cookies.r.us')->clear_cache(CLEAR_COOKIES());

This method returns L to aid in chaining methods.

=head2 clear_pref

accepts a L name and restores it to the original value.  See the L and L methods to get a preference value and to set to it to a particular value.  This method returns L to aid in chaining methods.

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

    $firefox->clear_pref('browser.search.defaultenginename');

=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 L. 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 Ltab handles|Firefox::Marionette::WebWindow>.

=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_bookmark

accepts a L as a parameter and deletes the bookmark from the Firefox database.

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

    my $firefox = Firefox::Marionette->new();
    foreach my $bookmark (reverse $firefox->bookmarks()) {
      if ($bookmark->parent_guid() ne Firefox::Marionette::Bookmark::ROOT()) {
        $firefox->delete_bookmark($bookmark);
      }
    }
    say "Bookmarks? We don't need no stinking bookmarks!";

This method returns L to aid in chaining methods.

=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! Note that this method will only delete cookies for the current site.  See L for an alternative.  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 delete_webauthn_all_credentials

This method accepts an optional L, in which case it will delete all L from this authenticator.  If no parameter is supplied, the default authenticator will have all credentials deleted.

    my $firefox = Firefox::Marionette->new();
    my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() );
    $firefox->delete_webauthn_all_credentials($authenticator);
    $firefox->delete_webauthn_all_credentials();

=head2 delete_webauthn_authenticator

This method accepts an optional L, in which case it will delete this authenticator from the current Firefox instance.  If no parameter is supplied, the default authenticator will be deleted.

    my $firefox = Firefox::Marionette->new();
    my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() );
    $firefox->delete_webauthn_authenticator($authenticator);
    $firefox->delete_webauthn_authenticator();

=head2 delete_webauthn_credential

This method accepts either a L and an L, in which case it will remove the credential from the supplied authenticator or

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();
    my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() );
    foreach my $credential ($firefox->webauthn_credentials($authenticator)) {
        $firefox->delete_webauthn_credential($credential, $authenticator);
    }

just a L, in which case it will remove the credential from the default authenticator.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();
    ...
    foreach my $credential ($firefox->webauthn_credentials()) {
        $firefox->delete_webauthn_credential($credential);
    }

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 displays

accepts an optional regex to filter against the L and returns a list of all the L as a L.

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

    my $firefox = Firefox::Marionette->new( visible => 1, kiosk => 1 )->go('http://metacpan.org');;
    my $element = $firefox->find_id('metacpan_search-input');
    foreach my $display ($firefox->displays(qr/iphone/smxi)) {
        say 'Can Firefox resize for "' . Encode::encode('UTF-8', $display->usage(), 1) . '"?';
        if ($firefox->resize($display->width(), $display->height())) {
            say 'Now displaying with a Pixel aspect ratio of ' . $display->par();
            say 'Now displaying with a Storage aspect ratio of ' . $display->sar();
            say 'Now displaying with a Display aspect ratio of ' . $display->dar();
        } else {
            say 'Apparently NOT!';
        }
    }

=head2 downloaded

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('page-content')->find_id('metacpan_search-input')->type('Test::More');

    $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();

    $firefox->find_partial('Download')->click();

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

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

        my $handle = $firefox->downloaded($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('page-content')->find_id('metacpan_search-input')->type('Test::More');

    $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();

    $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('page-content')->find_id('metacpan_search-input')->type('Test::More');

    $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();

    $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 searches 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 geo

accepts an optional L object or the parameters for a L object, turns on the L and returns the current L returned by calling the javascript L method.  This method is further discussed in the L section.  If the current location cannot be determined, this method will return undef.

NOTE: firefox will only allow L calls to be made from L and bizarrely, this does not include about:blank or similar.  Therefore, you will need to load a page before calling the L method.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new( proxy => 'https://this.is.another.location:3128', geo => 1 );

    # Get geolocation for this.is.another.location (via proxy)

    $firefox->geo($firefox->json('https://freeipapi.com/api/json/'));

    # now google maps will show us in this.is.another.location

    $firefox->go('https://maps.google.com/');

    if (my $geo = $firefox->geo()) {
        warn "Apparently, we're now at " . join q[, ], $geo->latitude(), $geo->longitude();
    } else {
        warn "This computer is not allowing geolocation";
    }

    # OR the quicker setup (run this with perl -C)

    warn "Apparently, we're now at " . Firefox::Marionette->new( proxy => 'https://this.is.another.location:3128', geo => 'https://freeipapi.com/api/json/' )->go('https://maps.google.com/')->geo();

NOTE: currently this call sets the location to be exactly what is specified.  It will also attempt to modify the current timezone (if available in the L parameter) to match the specified L.  This function should be considered experimental.  Feedback welcome.

If particular, the L is the only API that currently providing geolocation data and matching timezone data in one API call.  If anyone finds/develops another similar API, I would be delighted to include support for it in this module.

=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 and the L method for an alternative.

This method returns L to aid in chaining methods.

=head2 get_pref

accepts a L name.  See the L and L methods to set a preference value and to restore it to it's original value.  This method returns the current value of the preference.

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

    warn "Your browser's default search engine is set to " . $firefox->get_pref('browser.search.defaultenginename');

=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.

    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->await(sub { $firefox->find_class('autocomplete-suggestion'); })->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 alternative 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 import_bookmarks

accepts a filesystem path to a bookmarks file and imports all the L in that file.  It can deal with backups from L, L or Edge.

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

    my $firefox = Firefox::Marionette->new()->import_bookmarks('/path/to/bookmarks_file.html');

This method returns L to aid in chaining methods.

=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 existence 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('metacpan_search-input')->type('Type::More');
    $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->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 is_trusted

accepts an L as the first parameter.  This method returns true or false depending on if the certificate is a trusted CA certificate in the current profile.

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

    my $firefox = Firefox::Marionette->new( profile_name => 'default' );
    foreach my $certificate ($firefox->certificates()) {
        if (($certificate->is_ca_cert()) && ($firefox->is_trusted($certificate))) {
            say $certificate->display_name() . " is a trusted CA cert in the current profile";
        } 
    } 

=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};

In addition, this method can accept a L as a parameter and retrieve that URI via the firefox L and transforming the body to L

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

    say Firefox::Marionette->new()->json('https://freeipapi.com/api/json/')->{ipAddress};

=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 languages

accepts an optional list of values for the L header and sets this using the profile preferences.  It returns the current values as a list, such as ('en-US', 'en').

=head2 loaded

returns true if C

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    $firefox->find_id('metacpan_search-input')->type('Type::More');
    $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();
    while(!$firefox->loaded()) {
        # redirecting to Test::More page
    }

=head2 logins

returns a list of all L objects available.

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

    my $firefox = Firefox::Marionette->new();
    foreach my $login ($firefox->logins()) {
       say "Found login for " . $login->host() . " and user " . $login->user();
    }

=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_xml

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

=over 4

=item * L

=back

returns a list of L objects.

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

    my $handle = FileHandle->new('/path/to/keepass1.xml');
    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 latest 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).  See L for a discussion of how to send log messages to the console.

=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.  If this option is not a boolean (0|1), the value will be passed to the L option on the command line of the firefox binary to allow extra levels of debug.

=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 * geo - setup the browser L to allow the L to work.  If the value for this key is a L object or a string beginning with '^(?:data|http)', this object will be retrieved using the L method and the response will used to build a L object, which will be sent to the L method.  If the value for this key is a hash, the hash will be used to build a L object, which will be sent to the L method.

=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 L<-safe-mode|http://kb.mozillazine.org/Command_line_arguments#List_of_command_line_arguments_.28incomplete.29> 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 and L.  The user will default to the current user name (see the user parameter to change this).  Authentication should be via public keys loaded into the local 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 * index - a parameter to allow the user to specify a specific firefox instance to survive and reconnect to.  It does not do anything else at the moment.  See the survive parameter.

=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 and 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, instead, use the L and L methods to make changes while firefox 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. The following conditions are required to use existing profiles;

=over 8

=item * the preference C must be set to C in the profile OR

=item * the C parameter to this method must be set to C<0>

=back

NOTE: firefox ignores any changes made to the profile on the disk while it is running, instead, use the L and L methods to make changes while firefox is running.

=item * proxy - this is a shortcut method for setting a L using the L parameter above.  It accepts a proxy URL, with the following allowable schemes, 'http' and 'https'.  It also allows a reference to a list of proxy URLs which will function as list of proxies that Firefox will try in L until a working proxy is found.  See L, L and L.

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

=item * scp - force the scp protocol when transferring files to remote hosts via ssh. See L and the --scp-only option in the L script in this distribution.

=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 * stealth - stops L from being accessible by the current web page.  This is achieved by loading an L, which will automatically switch on the C parameter for the L method.  This is extremely experimental.  See L for a discussion.

=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 * trackable - if this is set, profile preferences will be L to make it harder to be tracked by the L across browser restarts.  This is on by default, but may be switched off by setting it to 0;

=item * user - if the "host" parameter is also set, use L to create and automate firefox with the specified user.  See L and L.  The user will default to the current user name.  Authentication should be via public keys loaded into the local L.

=item * via - specifies a L to be used to connect to a remote host.  See the host parameter.

=item * visible - should firefox be visible on the desktop.  This defaults to "0".  When moving from a X11 platform to another X11 platform, you can set visible to 'local' to enable L.  See L.

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

=item * webauthn - a boolean parameter to determine whether or not to L after the connection is established.  The default is to add a webauthn authenticator for Firefox after version 118.

=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 => 'timestamp,nsHttp: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 L 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.  C should be disabled to make C work.

=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 percentage_visible

accepts an L as the first parameter and returns the percentage of that L that is currently visible in the L.  It achieves this by determining the co-ordinates of the L with a L call and then using L and L calls to determine how the percentage of the L that is visible to the user.  The L call is used to determine the state of the L and L attributes.

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

    my $firefox = Firefox::Marionette->new( visible => 1, kiosk => 1 )->go('http://metacpan.org');;
    my $element = $firefox->find_id('metacpan_search-input');
    my $totally_viewable_percentage = $firefox->percentage_visible($element); # search box is slightly hidden by different effects
    foreach my $display ($firefox->displays()) {
        if ($firefox->resize($display->width(), $display->height())) {
            if ($firefox->percentage_visible($element) < $totally_viewable_percentage) {
               say 'Search box stops being fully viewable with ' . Encode::encode('UTF-8', $display->usage());
               last;
            }
        }
    }

=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 from the HTML source code.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
    my $element = $firefox->find_id('metacpan_search-input');
    $element->property('value') eq '' or die "Initial property should be 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 resize

accepts width and height parameters in a list and then attempts to resize the entire browser to match those parameters.  Due to the oddities of various window managers, this function needs to manually calculate what the maximum and minimum sizes of the display is.  It does this by;

=over 4

=item 1 performing a L, then

=item 2 caching the browser's current width and height as the maximum width and height. It

=item 3 then calls L to resize the window to 0,0

=item 4 wait for the browser to send a L event.

=item 5 cache the browser's current width and height as the minimum width and height

=item 6 if the requested width and height are outside of the maximum and minimum widths and heights return false

=item 7 if the requested width and height matches the current width and height return L to aid in chaining methods. Otherwise,

=item 8 call L for the requested width and height

=item 9 wait for the L event

=back

This method returns L to aid in chaining methods if the method succeeds, otherwise it returns false.

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

    my $firefox = Firefox::Marionette->new( visible => 1, kiosk => 1 )->go('http://metacpan.org');;
    if ($firefox->resize(1024, 768)) {
        say 'We are showing an XGA display';
    } else {
       say 'Resize failed to work';
    }

=head2 resolve

accepts a hostname as an argument and resolves it to a list of matching IP addresses.  It can also accept an optional hash containing additional keys, described in L.

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

    my $ssh_server = 'remote.example.org';
    my $firefox = Firefox::Marionette->new( host => $ssh_server );
    my $hostname = 'metacpan.org';
    foreach my $ip_address ($firefox->resolve($hostname)) {
       say "$hostname resolves to $ip_address at $ssh_server";
    }
    $firefox = Firefox::Marionette->new();
    foreach my $ip_address ($firefox->resolve($hostname, flags => Firefox::Marionette::DNS::RESOLVE_REFRESH_CACHE() | Firefox::Marionette::DNS::RESOLVE_BYPASS_CACHE(), type => Firefox::Marionette::DNS::RESOLVE_TYPE_DEFAULT())) {
       say "$hostname resolves to $ip_address;
    }


=head2 resolve_override

accepts a hostname and an IP address as parameters.  This method then forces the browser to override any future DNS requests for the supplied hostname.

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

    my $firefox = Firefox::Marionette->new();
    my $hostname = 'metacpan.org';
    my $ip_address = '127.0.0.1';
    foreach my $result ($firefox->resolve_override($hostname, $ip_address)->resolve($hostname)) {
       if ($result eq $ip_address) {
         warn "local metacpan time?";
       } else {
         die "This should not happen";
       }
    }
    $firefox->go('https://metacpan.org'); # this tries to contact a webserver on 127.0.0.1

This method returns L to aid in chaining methods.

=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 evaluated 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 evaluated 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("metacpan_search-input")[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 scroll

accepts a L as the first parameter and L to it.  The optional second parameter is the same as for the L method.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new(visible => 1)->go('https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView');
    my $link = $firefox->find_id('content')->find_link('Examples');
    $firefox->scroll($link);
    $firefox->scroll($link, 1);
    $firefox->scroll($link, { behavior => 'smooth', block => 'center' });
    $firefox->scroll($link, { block => 'end', inline => 'nearest' });

=head2 send_alert_text

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

=head2 set_javascript

accepts a parameter for the the profile preference value of L.  This method returns L to aid in chaining methods.

=head2 set_pref

accepts a L name and the new value to set it to.  See the L and L methods to get a preference value and to restore it to it's original value.  This method returns L to aid in chaining methods.

    use Firefox::Marionette();
    my $firefox = Firefox::Marionette->new();
    ...
    $firefox->set_pref('browser.search.defaultenginename', 'DuckDuckGo');

=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();
    }

See the L section for how to delve into a L.

=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 alternative 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 L (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 = $firefox->window_handle();
    $firefox->new_window( type => 'tab' );
    $firefox->new_window( type => 'window' );
    $firefox->switch_to_window($original_window);
    $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 tz

accepts a L as the first parameter. This method returns L to aid in chaining methods.

=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 uname

returns the $^O ($OSNAME) compatible string to describe the platform where firefox is running.

=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 webauthn_authenticator

returns the default L created when the L method was called.

=head2 webauthn_credentials

This method accepts an optional L, in which case it will return all the L attached to this authenticator.  If no parameter is supplied, L from the default authenticator will be returned.

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

    my $firefox = Firefox::Marionette->new();
    foreach my $credential ($firefox->webauthn_credentials()) {
       say "Credential host is " . $credential->host();
    }

    # OR

    my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() );
    foreach my $credential ($firefox->webauthn_credentials($authenticator)) {
       say "Credential host is " . $credential->host();
    }

=head2 webauthn_set_user_verified

This method accepts a boolean for the L field and an optional L (the default authenticator will be used otherwise).  It sets the L field to the supplied boolean value.

    use Firefox::Marionette();

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

=head2 wheel

accepts a L parameter, or a C<( x =E 0, y =E 0 )> type hash manually describing exactly where to move the mouse from and returns an action for use in the L method that corresponding with such a wheel action, 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.

=item * deltaX - the change in X co-ordinates during the wheel.  If not defined, deltaX defaults to 0.

=item * deltaY - the change in Y co-ordinates during the wheel.  If not defined, deltaY defaults to 0.

=back

=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 existence of this product.  Only of interest when sub-classing.

=head2 window_handle

returns the L. 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.  This is the same as the L object in Javascript.

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new();
    my $original_window = $firefox->window_handle();
    my $javascript_window = $firefox->script('return window'); # only works for Firefox 121 and later
    if ($javascript_window ne $original_window) {
        die "That was unexpected!!! What happened?";
    }

=head2 window_handles

returns a list of top-level L. 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 = $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 NETWORK ARCHITECTURE

This module allows for a complicated network architecture, including SSH and HTTP proxies.

  my $firefox = Firefox::Marionette->new(
                  host  => 'Firefox.runs.here'
                  via   => 'SSH.Jump.Box',
                  trust => '/path/to/ca-for-squid-proxy-server.crt',
                  proxy => 'https://Squid.Proxy.Server:3128'
                     )->go('https://Target.Web.Site');

produces the following effect, with an ascii box representing a separate network node.

     ---------          ----------         -----------
     | Perl  |  SSH     | SSH    |  SSH    | Firefox |
     | runs  |--------->| Jump   |-------->| runs    |
     | here  |          | Box    |         | here    |
     ---------          ----------         -----------
                                                |
     ----------          ----------             |
     | Target |  HTTPS   | Squid  |    TLS      |
     | Web    |<---------| Proxy  |<-------------
     | Site   |          | Server |
     ----------          ----------

In addition, the proxy parameter can be used to specify multiple proxies using a reference
to a list.

  my $firefox = Firefox::Marionette->new(
                  host  => 'Firefox.runs.here'
                  trust => '/path/to/ca-for-squid-proxy-server.crt',
                  proxy => [ 'https://Squid1.Proxy.Server:3128', 'https://Squid2.Proxy.Server:3128' ]
                     )->go('https://Target.Web.Site');

When firefox gets a list of proxies, it will use the first one that works.  In addition, it will perform a basic form of proxy failover, which may involve a failed network request before it fails over to the next proxy.  In the diagram below, Squid1.Proxy.Server is the first proxy in the list and will be used exclusively, unless it is unavailable, in which case Squid2.Proxy.Server will be used.

                                          ----------
                                     TLS  | Squid1 |
                                   ------>| Proxy  |-----
                                   |      | Server |    |
     ---------      -----------    |      ----------    |       -----------
     | Perl  | SSH  | Firefox |    |                    | HTTPS | Target  |
     | runs  |----->| runs    |----|                    ------->| Web     |
     | here  |      | here    |    |                    |       | Site    |
     ---------      -----------    |      ----------    |       -----------
                                   | TLS  | Squid2 |    |
                                   ------>| Proxy  |-----
                                          | Server |
                                          ----------

See the L section for more options.

See L for easy proxying via L

See L section for how to combine this with providing appropriate browser settings for the end point.

=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 GEO LOCATION

The firefox L can be used by supplying the C parameter to the L method and then calling the L method (from a L).

The L method can accept various specific latitude and longitude parameters as a list, such as;

    $firefox->geo(latitude => -37.82896, longitude => 144.9811);

    OR

    $firefox->geo(lat => -37.82896, long => 144.9811);

    OR

    $firefox->geo(lat => -37.82896, lng => 144.9811);

    OR

    $firefox->geo(lat => -37.82896, lon => 144.9811);

or it can be passed in as a reference, such as;

    $firefox->geo({ latitude => -37.82896, longitude => 144.9811 });

the combination of a variety of parameter names and the ability to pass parameters in as a reference means it can be deal with various geo location websites, such as;

    $firefox->geo($firefox->json('https://freeipapi.com/api/json/')); # get geo location from current IP address

    $firefox->geo($firefox->json('https://geocode.maps.co/search?street=101+Collins+St&city=Melbourne&state=VIC&postalcode=3000&country=AU&format=json')->[0]); # get geo location of street address

    $firefox->geo($firefox->json('http://api.positionstack.com/v1/forward?access_key=' . $access_key . '&query=101+Collins+St,Melbourne,VIC+3000')->{data}->[0]); # get geo location of street address using api key

    $firefox->geo($firefox->json('https://api.ipgeolocation.io/ipgeo?apiKey=' . $api_key)); # get geo location from current IP address

    $firefox->geo($firefox->json('http://api.ipstack.com/142.250.70.206?access_key=' . $api_key)); # get geo location from specific IP address (http access only for free)

These sites were active at the time this documentation was written, but mainly function as an illustration of the flexibility of L and L methods in providing the desired location to the L.

As mentioned in the L method documentation, the L is the only API that currently providing geolocation data and matching timezone data in one API call.  If this url is used, the L method will be automatically called to set the timezone to the matching timezone for the geographic location.

=head1 CONSOLE LOGGING

Sending debug to the console can be quite confusing in firefox, as some techniques won't work in L context.  The following example can be quite useful.

    use Firefox::Marionette();

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

    $firefox->script( q[console.log("This goes to devtools b/c it's being generated in content mode")]);

    $firefox->chrome()->script( q[console.log("Sent out on standard error for Firefox 136 and later")]);

=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/');

    # OR use a proxy host to jump via to the final host

    my $firefox = Firefox::Marionette->new(
                                             host  => 'remote.example.org',
                                             port  => 2222,
                                             via   => 'user@secure-jump-box.example.org:42222',
                                             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.

The module will expect to access private keys via the local L when authenticating.

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.

The L section has an example of a more complicated network design.

=head1 WEBGL

There are a number of steps to getting L to work correctly;

=over

=item 1. The C parameter to the L method must be set.  This will disable L<-safe-mode|http://kb.mozillazine.org/Command_line_arguments#List_of_command_line_arguments_.28incomplete.29>

=item 2. The visible parameter to the L method must be set.  This is due to L.

=item 3. It can be tricky getting L to work with a L instance.  L can be useful to help debug issues in this case.  The mesa-dri-drivers rpm is also required for Redhat systems.

=back

With all those conditions being met, L can be enabled like so;

    use Firefox::Marionette();

    my $firefox = Firefox::Marionette->new( addons => 1, visible => 1 );
    if ($firefox->script(q[let c = document.createElement('canvas'); return c.getContext('webgl2') ? true : c.getContext('experimental-webgl') ? true : false;])) {
        $firefox->go("https://get.webgl.org/");
    } else {
        die "WebGL is not supported";
    }

=head1 FINDING ELEMENTS IN A SHADOW DOM

One aspect of L is the L.  When you need to explore the structure of a L, you need to access it via the shadow DOM.  The following is an example of navigating the shadow DOM via a html file included in the test suite of this package.

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

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

    my $shadow_root = $firefox->find_tag('custom-square')->shadow_root();

    my $outer_div = $firefox->find_id('outer-div', $shadow_root);

So, this module is designed to allow you to navigate the shadow DOM using normal find methods, but you must get the shadow element's shadow root and use that as the root for the search into the shadow DOM.  An important caveat is that L and L strategies do not officially work yet (and also the class name and name strategies).  This module works around the tag name, class name and name deficiencies by using the matching L search if the original search throws a recognisable exception.  Therefore these cases may be considered to be extremely experimental and subject to change when Firefox gets the "correct" functionality.

=head1 IMITATING OTHER BROWSERS

There are a collection of methods and techniques that may be useful if you would like to change your geographic location or how the browser appears to your web site.

=over

=item * the C parameter of the L method.  This method will stop the browser reporting itself as a robot and will also (when combined with the L method, change other javascript characteristics to match the L string.

=item * the L method, which if supplied a recognisable L, will attempt to change other attributes to match the desired browser.  This is extremely experimental and feedback is welcome.

=item * the L method, which allows the modification of the L reported by the browser, but not the location produced by mapping the external IP address used by the browser (see the L section for a discussion of different types of proxies that can be used to change your external IP address).

=item * the L method, which can change the L for your browser session.

=item * the L method, which can change the L for your browser session.

=back

This list of methods may grow.

=head1 WEBSITES THAT BLOCK AUTOMATION

Marionette L allows web sites to detect that the browser is being automated.  Firefox L allows you to disable this functionality while you are automating the browser, but this can be overridden with the C parameter for the L method.  This is extremely experimental and feedback is welcome.

If the web site you are trying to automate mysteriously fails when you are automating a workflow, but it works when you perform the workflow manually, you may be dealing with a web site that is hostile to automation.  I would be very interested if you can supply a test case.

At the very least, under these circumstances, it would be a good idea to be aware that there's an L, and potential L in this area.

=head1 X11 FORWARDING WITH FIREFOX

L allows you to launch a L and have it visually appear in your local X11 desktop.  This can be accomplished with the following code;

    use Firefox::Marionette();

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

Feedback is welcome on any odd X11 workarounds that might be required for different platforms.

=head1 UBUNTU AND FIREFOX DELIVERED VIA SNAP

L is packaging firefox as a L.  This breaks the way that this module expects to be able to run, specifically, being able to setup a firefox profile in a systems temporary directory (/tmp or $TMPDIR in most Unix based systems) and allow the operating system to cleanup old directories caused by exceptions / network failures / etc.

Because of this design decision, attempting to run a snap version of firefox will simply result in firefox hanging, unable to read it's custom profile directory and hence unable to read the marionette port configuration entry.

Which would be workable except that; there does not appear to be _any_ way to detect that a snap firefox will run (/usr/bin/firefox is a bash shell which eventually runs the snap firefox), so there is no way to know (heuristics aside) if a normal firefox or a snap firefox will be launched by execing 'firefox'.

It seems the only way to fix this issue (as documented in more than a few websites) is;

=over

=item 1. sudo snap remove firefox

=item 2. sudo add-apt-repository -y ppa:mozillateam/ppa

=item 3. sudo apt update

=item 4. sudo apt install -t 'o=LP-PPA-mozillateam' firefox

=item 5. echo -e "Package: firefox*\nPin: release o=LP-PPA-mozillateam\nPin-Priority: 501" >/tmp/mozillateamppa

=item 6. sudo mv /tmp/mozillateamppa /etc/apt/preferences.d/mozillateamppa

=back

If anyone is aware of a reliable method to detect if a snap firefox is going to launch vs a normal firefox, I would love to know about it.

This technique is used in the L script in this distribution.

=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
 
=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();

Also, check out the L method for an alternative.

=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

=back

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2024, 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.63/lib/Waterfox/0000755000175000017500000000000014763402246016147 5ustar  davedaveFirefox-Marionette-1.63/lib/Waterfox/Marionette/0000755000175000017500000000000014763402246020256 5ustar  davedaveFirefox-Marionette-1.63/lib/Waterfox/Marionette/Profile.pm0000644000175000017500000001666514763400572022232 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.63';

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.63

=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) 2024, 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.63/lib/Waterfox/Marionette.pm0000644000175000017500000001303214763400572020613 0ustar  davedavepackage Waterfox::Marionette;

use warnings;
use strict;
use English qw( -no_match_vars );
use Waterfox::Marionette::Profile();
use parent 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.63';

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'         => 'WaterfoxLimited',
    '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.63

=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('page-content')->find_id('metacpan_search-input')->type('Test::More');

    say "Height of page-content div is " . $waterfox->find_class('page-content')->css('height');

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

    $waterfox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();

    $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) 2024, 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.63/README0000644000175000017500000045311314763400566014474 0ustar  davedaveNAME

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

VERSION

    Version 1.63

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('page-content')->find_id('metacpan_search-input')->type('Test::More');
    
        say "Height of page-content div is " . $firefox->find_class('page-content')->css('height');
    
        my $file_handle = $firefox->selfie();
    
        $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();
    
        $firefox->find_partial('Download')->click();

DESCRIPTION

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

CONSTANTS

 BCD_PATH

    returns the local path used for storing the brower compability data for
    the agent method when the stealth parameter is supplied to the new
    method. This database is built by the build-bcd-for-firefox
     binary.

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_bookmark

    accepts a bookmark as a parameter and adds the specified bookmark to
    the Firefox places database.

        use Firefox::Marionette();
    
        my $bookmark = Firefox::Marionette::Bookmark->new(
                         url   => 'https://metacpan.org',
                         title => 'This is MetaCPAN!'
                                 );
        my $firefox = Firefox::Marionette->new()->add_bookmark($bookmark);

    This method returns itself to aid in chaining methods.

 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 containing 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
    
        my $form_login = Firefox::Marionette::Login(host => 'https://github.com', user => 'me2@example.org', password => 'uiop[]', user_field => 'login', password_field => 'password');
        $firefox->add_login($form_login);
    
        # or just directly
    
        $firefox->add_login(host => 'https://github.com', user => 'me2@example.org', password => 'uiop[]', user_field => 'login', password_field => 'password');

    Note for HTTP Authentication, the realm
     must
    perfectly match the correct realm supplied by the server.

    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

 add_webauthn_authenticator

    accepts a hash of the following keys;

      * has_resident_key - boolean value to indicate if the authenticator
      will support client side discoverable credentials
      

      * has_user_verification - boolean value to determine if the
      authenticator
       supports
      user verification
      .

      * is_user_consenting - boolean value to determine the result of all
      user consent 
      authorization gestures
      , and by
      extension, any test of user presence
       performed
      on the Virtual Authenticator
      . If set to
      true, a user consent will always be granted. If set to false, it will
      not be granted.

      * is_user_verified - boolean value to determine the result of User
      Verification 
      performed on the Virtual Authenticator
      . If set to
      true, User Verification will always succeed. If set to false, it will
      fail.

      * protocol - the protocol spoken by the authenticator. This may be
      CTAP1_U2F, CTAP2 or CTAP2_1.

      * transport - the transport simulated by the authenticator. This may
      be BLE, HYBRID, INTERNAL, NFC, SMART_CARD or USB.

    It returns the newly created authenticator.

        use Firefox::Marionette();
        use Crypt::URandom();
    
        my $user_name = MIME::Base64::encode_base64( Crypt::URandom::urandom( 10 ), q[] ) . q[@example.com];
        my $firefox = Firefox::Marionette->new( webauthn => 0 );
        my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() );
        $firefox->go('https://webauthn.io');
        $firefox->find_id('input-email')->type($user_name);
        $firefox->find_id('register-button')->click();
        $firefox->await(sub { sleep 1; $firefox->find_class('alert-success'); });
        $firefox->find_id('login-button')->click();
        $firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); });

 add_webauthn_credential

    accepts a hash of the following keys;

      * authenticator - contains the authenticator that the credential will
      be added to. If this parameter is not supplied, the credential will
      be added to the default authenticator, if one exists.

      * host - contains the domain that this credential is to be used for.
      In the language of WebAuthn , this
      field is referred to as the relying party identifier
       or RP ID
      .

      * id - contains the unique id for this credential, also known as the
      Credential ID . If
      this is not supplied, one will be generated.

      * is_resident - contains a boolean that if set to true, a client-side
      discoverable credential
      
      is created. If set to false, a server-side credential
       is created
      instead.

      * private_key - either a RFC5958
       encoded private key encoded
      using encode_base64url or a hash containing the following keys;

	* name - contains the name of the private key algorithm, such as
	"RSA-PSS" (the default), "RSASSA-PKCS1-v1_5", "ECDSA" or "ECDH".

	* size - contains the modulus length of the private key. This is
	only valid for "RSA-PSS" or "RSASSA-PKCS1-v1_5" private keys.

	* hash - contains the name of the hash algorithm, such as "SHA-512"
	(the default). This is only valid for "RSA-PSS" or
	"RSASSA-PKCS1-v1_5" private keys.

	* curve - contains the name of the curve for the private key, such
	as "P-384" (the default). This is only valid for "ECDSA" or "ECDH"
	private keys.

      * sign_count - contains the initial value for a signature counter
       associated to the
      public key credential source
      . It
      will default to 0 (zero).

      * user - contains the userHandle
      
      associated to the credential encoded using encode_base64url. This
      property is optional.

    It returns the newly created credential. If of course, the credential
    is just created, it probably won't be much good by itself. However, you
    can use it to recreate a credential, so long as you know all the
    parameters.

        use Firefox::Marionette();
        use Crypt::URandom();
    
        my $user_name = MIME::Base64::encode_base64( Crypt::URandom::urandom( 10 ), q[] ) . q[@example.com];
        my $firefox = Firefox::Marionette->new();
        $firefox->go('https://webauthn.io');
        $firefox->find_id('input-email')->type($user_name);
        $firefox->find_id('register-button')->click();
        $firefox->await(sub { sleep 1; $firefox->find_class('alert-success'); });
        $firefox->find_id('login-button')->click();
        $firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); });
        foreach my $credential ($firefox->webauthn_credentials()) {
            $firefox->delete_webauthn_credential($credential);

    # ... time passes ...

            $firefox->add_webauthn_credential(
                      id            => $credential->id(),
                      host          => $credential->host(),
                      user          => $credential->user(),
                      private_key   => $credential->private_key(),
                      is_resident   => $credential->is_resident(),
                      sign_count    => $credential->sign_count(),
                                  );
        }
        $firefox->go('about:blank');
        $firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_COOKIES());
        $firefox->go('https://webauthn.io');
        $firefox->find_id('input-email')->type($user_name);
        $firefox->find_id('login-button')->click();
        $firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); });

 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.

 agent

    accepts an optional value for the User-Agent
    
    header and sets this using the profile preferences and inserting
    javascript into the current page. It returns the current value, such as
    'Mozilla/5.0 ()  ()
    '. This value is retrieved with navigator.userAgent
    .

    This method can be used to set a user agent string like so;

        use Firefox::Marionette();
        use strict;
    
        # useragents.me should only be queried once a month or less.
        # these UA strings should be cached locally.
    
        my %user_agent_strings = map { $_->{ua} => $_->{pct} } @{$firefox->json("https://www.useragents.me/api")->{data}};
        my ($user_agent) = reverse sort { $user_agent_strings{$a} <=> $user_agent_strings{$b} } keys %user_agent_strings;
    
        my $firefox = Firefox::Marionette->new();
        $firefox->agent($user_agent); # agent is now the most popular agent from useragents.me

    If the user agent string that is passed as a parameter looks like a
    Chrome , Edge
     or Safari 
    user agent string, then this method will also try and change other
    profile preferences to match the new agent string. These parameters
    are;

      * general.appversion.override

      * general.oscpu.override

      * general.platform.override

      * network.http.accept

      * network.http.accept-encoding

      * network.http.accept-encoding.secure

      * privacy.donottrackheader.enabled

    In addition, this method will accept a hash of values as parameters as
    well. When a hash is provided, this method will alter specific parts of
    the normal Firefox User Agent. These hash parameters are;

      * os - The desired operating system, known values are "linux",
      "win32", "darwin", "freebsd", "netbsd", "openbsd" and "dragonfly"

      * version - A specific version of firefox, such as 120.

      * arch - A specific version of the architecture, such as "x86_64" or
      "aarch64" or "s390x".

      * increment - A specific offset from the actual version of firefox,
      such as -5

    These parameters can be used to set a user agent string like so;

        use Firefox::Marionette();
        use strict;
    
        my $firefox = Firefox::Marionette->new();
        $firefox->agent(os => 'freebsd', version => 118);
    
        # user agent is now equal to
        # Mozilla/5.0 (X11; FreeBSD amd64; rv:109.0) Gecko/20100101 Firefox/118.0
    
        $firefox->agent(os => 'linux', arch => 's390x', version => 115);
        # user agent is now equal to
        # Mozilla/5.0 (X11; Linux s390x; rv:109.0) Gecko/20100101 Firefox/115.0

    If the stealth parameter has supplied to the new method, it will also
    attempt to create known specific javascript functions to imitate the
    required browser. If the database built by build-bcd-for-firefox
     is accessible, then it
    will also attempt to delete/provide dummy implementations for the
    corresponding javascript attributes
     for the desired browser.
    The following websites have been very useful in testing these ideas;

      * https://browserleaks.com/javascript

      * https://www.amiunique.org/fingerprint

      * https://bot.sannysoft.com/

      * https://lraj22.github.io/browserfeatcl/

    Importantly, this will break feature detection
    
    for any website that relies on it.

    See IMITATING OTHER BROWSERS a discussion of these types of techniques.
    These changes are not foolproof, but it is interesting to see what can
    be done with modern browsers. All this behaviour should be regarded as
    extremely experimental and subject to change. Feedback welcome.

 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'.

 arch

    returns the architecture of the machine running firefox. Should be
    something like 'x86_64' or 'arm'. This is only intended for test suite
    support.

 aria_label

    accepts an element as the parameter. It returns the ARIA label
    
    for the element.

 aria_role

    accepts an element as the parameter. It returns the ARIA role
    
    for the element.

 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
    from the HTML source code, 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('metacpan_search-input');
        !defined $element->attribute('value') or die "attribute is defined but did not exist in the html source!";
        $element->type('Test::More');
        !defined $element->attribute('value') or die "attribute has changed but only the property should have changed!";

 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->await(sub { $firefox->find_class('autocomplete-suggestion'); })->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.

 download

    accepts a URI and an optional timeout in seconds (the default is 5
    minutes) as parameters and downloads the URI in the background and
    returns a handle to the downloaded file.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new();
    
        my $handle = $firefox->download('https://raw.githubusercontent.com/david-dick/firefox-marionette/master/t/data/keepassxs.csv');
    
        foreach my $line (<$handle>) {
          print $line;
        }

 bookmarks

    accepts either a scalar or a hash as a parameter. The scalar may by the
    title of a bookmark or the URL of the bookmark. The hash may have the
    following keys;

      * title - The title of the bookmark.

      * url - The url of the bookmark.

    returns a list of all Firefox::Marionette::Bookmark objects that match
    the supplied parameters (if any).

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new();
    
        foreach my $bookmark ($firefox->bookmarks(title => 'This is MetaCPAN!')) {
          say "Bookmark found";
        }
    
        # OR
    
        foreach my $bookmark ($firefox->bookmarks()) {
          say "Bookmark found with URL " . $bookmark->url();
        }
    
        # OR
    
        foreach my $bookmark ($firefox->bookmarks('https://metacpan.org')) {
          say "Bookmark found";
        }

 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->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();
    
        $firefox->bye(sub { $firefox->find_name('metacpan_search-input') })->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();

 cache_keys

    returns the set of all cache keys from Firefox::Marionette::Cache.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
        foreach my $key_name ($firefox->cache_keys()) {
          my $key_value = $firefox->check_cache_key($key_name);
          if (Firefox::Marionette::Cache->$key_name() != $key_value) {
            warn "This module this the value of $key_name is " . Firefox::Marionette::Cache->$key_name();
            warn "Firefox thinks the value of   $key_name is $key_value";
          }
        }

 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";
            print 'PEM Encoded Certificate ' . "\n" . $firefox->certificate_as_pem($certificate) . "\n";
        }

    This method returns itself to aid in chaining methods.

 check_cache_key

    accepts a cache_key as a parameter.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
        foreach my $key_name ($firefox->cache_keys()) {
          my $key_value = $firefox->check_cache_key($key_name);
          if (Firefox::Marionette::Cache->$key_name() != $key_value) {
            warn "This module this the value of $key_name is " . Firefox::Marionette::Cache->$key_name();
            warn "Firefox thinks the value of   $key_name is $key_value";
          }
        }

    This method returns the cache_key's actual value from firefox as a
    number. This may differ from the current value of the key from
    Firefox::Marionette::Cache as these values have changed as firefox has
    evolved.

 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 a server-assigned identifier 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

 clear_cache

    accepts a single flag parameter, which can be an ORed set of keys from
    Firefox::Marionette::Cache and clears the appropriate sections of the
    cache. If no flags parameter is supplied, the default is CLEAR_ALL.
    Note that this method, unlike delete_cookies will actually delete all
    cookies for all hosts, not just the current webpage.

        use Firefox::Marionette();
        use Firefox::Marionette::Cache qw(:all);
    
        my $firefox = Firefox::Marionette->new()->go('https://do.lots.of.evil/')->clear_cache(); # default clear all
    
        $firefox->go('https://cookies.r.us')->clear_cache(CLEAR_COOKIES());

    This method returns itself to aid in chaining methods.

 clear_pref

    accepts a preference  name and
    restores it to the original value. See the get_pref and set_pref
    methods to get a preference value and to set to it to a particular
    value. This method returns itself to aid in chaining methods.

        use Firefox::Marionette();
        my $firefox = Firefox::Marionette->new();
    
        $firefox->clear_pref('browser.search.defaultenginename');

 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_bookmark

    accepts a bookmark as a parameter and deletes the bookmark from the
    Firefox database.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new();
        foreach my $bookmark (reverse $firefox->bookmarks()) {
          if ($bookmark->parent_guid() ne Firefox::Marionette::Bookmark::ROOT()) {
            $firefox->delete_bookmark($bookmark);
          }
        }
        say "Bookmarks? We don't need no stinking bookmarks!";

    This method returns itself to aid in chaining methods.

 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! Note that this method will only delete cookies
    for the current site. See clear_cache for an alternative. 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.

 delete_webauthn_all_credentials

    This method accepts an optional authenticator, in which case it will
    delete all credentials from this authenticator. If no parameter is
    supplied, the default authenticator will have all credentials deleted.

        my $firefox = Firefox::Marionette->new();
        my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() );
        $firefox->delete_webauthn_all_credentials($authenticator);
        $firefox->delete_webauthn_all_credentials();

 delete_webauthn_authenticator

    This method accepts an optional authenticator, in which case it will
    delete this authenticator from the current Firefox instance. If no
    parameter is supplied, the default authenticator will be deleted.

        my $firefox = Firefox::Marionette->new();
        my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() );
        $firefox->delete_webauthn_authenticator($authenticator);
        $firefox->delete_webauthn_authenticator();

 delete_webauthn_credential

    This method accepts either a credential and an authenticator, in which
    case it will remove the credential from the supplied authenticator or

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
        my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() );
        foreach my $credential ($firefox->webauthn_credentials($authenticator)) {
            $firefox->delete_webauthn_credential($credential, $authenticator);
        }

    just a credential, in which case it will remove the credential from the
    default authenticator.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
        ...
        foreach my $credential ($firefox->webauthn_credentials()) {
            $firefox->delete_webauthn_credential($credential);
        }

    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

 displays

    accepts an optional regex to filter against the usage for the display
    and returns a list of all the known displays
     as a
    Firefox::Marionette::Display.

        use Firefox::Marionette();
        use Encode();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new( visible => 1, kiosk => 1 )->go('http://metacpan.org');;
        my $element = $firefox->find_id('metacpan_search-input');
        foreach my $display ($firefox->displays(qr/iphone/smxi)) {
            say 'Can Firefox resize for "' . Encode::encode('UTF-8', $display->usage(), 1) . '"?';
            if ($firefox->resize($display->width(), $display->height())) {
                say 'Now displaying with a Pixel aspect ratio of ' . $display->par();
                say 'Now displaying with a Storage aspect ratio of ' . $display->sar();
                say 'Now displaying with a Display aspect ratio of ' . $display->dar();
            } else {
                say 'Apparently NOT!';
            }
        }

 downloaded

    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('page-content')->find_id('metacpan_search-input')->type('Test::More');
    
        $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();
    
        $firefox->find_partial('Download')->click();
    
        while(!$firefox->downloads()) { sleep 1 }
    
        foreach my $path ($firefox->downloads()) {
    
            my $handle = $firefox->downloaded($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('page-content')->find_id('metacpan_search-input')->type('Test::More');
    
        $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();
    
        $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('page-content')->find_id('metacpan_search-input')->type('Test::More');
    
        $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();
    
        $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 searches 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.

 geo

    accepts an optional geo location object or the parameters for a geo
    location object, turns on the Geolocation API
     and
    returns the current value returned by calling the javascript
    getCurrentPosition
    
    method. This method is further discussed in the GEO LOCATION section.
    If the current location cannot be determined, this method will return
    undef.

    NOTE: firefox will only allow Geolocation
     calls to
    be made from secure contexts
    
    and bizarrely, this does not include about:blank or similar. Therefore,
    you will need to load a page before calling the geo method.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new( proxy => 'https://this.is.another.location:3128', geo => 1 );
    
        # Get geolocation for this.is.another.location (via proxy)
    
        $firefox->geo($firefox->json('https://freeipapi.com/api/json/'));
    
        # now google maps will show us in this.is.another.location
    
        $firefox->go('https://maps.google.com/');
    
        if (my $geo = $firefox->geo()) {
            warn "Apparently, we're now at " . join q[, ], $geo->latitude(), $geo->longitude();
        } else {
            warn "This computer is not allowing geolocation";
        }
    
        # OR the quicker setup (run this with perl -C)
    
        warn "Apparently, we're now at " . Firefox::Marionette->new( proxy => 'https://this.is.another.location:3128', geo => 'https://freeipapi.com/api/json/' )->go('https://maps.google.com/')->geo();

    NOTE: currently this call sets the location to be exactly what is
    specified. It will also attempt to modify the current timezone (if
    available in the geo location parameter) to match the specified
    timezone. This function should be considered experimental. Feedback
    welcome.

    If particular, the ipgeolocation API
     is the
    only API that currently providing geolocation data and matching
    timezone data in one API call. If anyone finds/develops another similar
    API, I would be delighted to include support for it in this module.

 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 and the download method
    for an alternative.

    This method returns itself to aid in chaining methods.

 get_pref

    accepts a preference  name. See
    the set_pref and clear_pref methods to set a preference value and to
    restore it to it's original value. This method returns the current
    value of the preference.

        use Firefox::Marionette();
        my $firefox = Firefox::Marionette->new();
    
        warn "Your browser's default search engine is set to " . $firefox->get_pref('browser.search.defaultenginename');

 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.

        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->await(sub { $firefox->find_class('autocomplete-suggestion'); })->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 alternative 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();

 import_bookmarks

    accepts a filesystem path to a bookmarks file and imports all the
    bookmarks in that file. It can deal with backups from Firefox
    ,
    Chrome  or Edge.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new()->import_bookmarks('/path/to/bookmarks_file.html');

    This method returns itself to aid in chaining methods.

 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 existence 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('metacpan_search-input')->type('Type::More');
        $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->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.

 is_trusted

    accepts an certificate as the first parameter. This method returns true
    or false depending on if the certificate is a trusted CA certificate in
    the current profile.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new( profile_name => 'default' );
        foreach my $certificate ($firefox->certificates()) {
            if (($certificate->is_ca_cert()) && ($firefox->is_trusted($certificate))) {
                say $certificate->display_name() . " is a trusted CA cert in the current profile";
            } 
        } 

 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};

    In addition, this method can accept a URI as a parameter and retrieve
    that URI via the firefox fetch call
     and
    transforming the body to JSON via firefox
    

        use Firefox::Marionette();
        use v5.10;
    
        say Firefox::Marionette->new()->json('https://freeipapi.com/api/json/')->{ipAddress};

 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();

 languages

    accepts an optional list of values for the Accept-Language
    
    header and sets this using the profile preferences. It returns the
    current values as a list, such as ('en-US', 'en').

 loaded

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

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        $firefox->find_id('metacpan_search-input')->type('Type::More');
        $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click();
        while(!$firefox->loaded()) {
            # redirecting to Test::More page
        }

 logins

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

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new();
        foreach my $login ($firefox->logins()) {
           say "Found login for " . $login->host() . " and user " . $login->user();
        }

 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_xml

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

      * KeePass 1.x XML
      

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

        use Firefox::Marionette();
        use FileHandle();
    
        my $handle = FileHandle->new('/path/to/keepass1.xml');
        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 latest 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). See CONSOLE
      LOGGING for a discussion of how to send log messages to the console.

      * 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. If this option is not a boolean (0|1), the value will be
      passed to the MOZ_LOG
      
      option on the command line of the firefox binary to allow extra
      levels of debug.

      * 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.

      * geo - setup the browser preferences
       to allow the Geolocation API
       to
      work. If the value for this key is a URI object or a string beginning
      with '^(?:data|http)', this object will be retrieved using the json
      method and the response will used to build a GeoLocation object,
      which will be sent to the geo method. If the value for this key is a
      hash, the hash will be used to build a GeoLocation object, which will
      be sent to the geo method.

      * 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 and NETWORK ARCHITECTURE. The user will default to
      the current user name (see the user parameter to change this).
      Authentication should be via public keys loaded into the local
      ssh-agent .

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

      * index - a parameter to allow the user to specify a specific firefox
      instance to survive and reconnect to. It does not do anything else at
      the moment. See the survive parameter.

      * 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 and
      NETWORK ARCHITECTURE.

      * 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, instead, use the set_pref and clear_pref methods to make
      changes while firefox 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. The following
      conditions are required to use existing profiles;

	* the preference security.webauth.webauthn_enable_softtoken must be
	set to true in the profile OR

	* the webauth parameter to this method must be set to 0

      NOTE: firefox ignores any changes made to the profile on the disk
      while it is running, instead, use the set_pref and clear_pref methods
      to make changes while firefox is running.

      * proxy - this is a shortcut method for setting a proxy using the
      capabilities parameter above. It accepts a proxy URL, with the
      following allowable schemes, 'http' and 'https'. It also allows a
      reference to a list of proxy URLs which will function as list of
      proxies that Firefox will try in left to right order
      
      until a working proxy is found. See REMOTE AUTOMATION OF FIREFOX VIA
      SSH, NETWORK ARCHITECTURE and SETTING UP SOCKS SERVERS USING SSH.

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

      * scp - force the scp protocol when transferring files to remote
      hosts via ssh. See REMOTE AUTOMATION OF FIREFOX VIA SSH and the
      --scp-only option in the ssh-auth-cmd-marionette
       script in this
      distribution.

      * 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.

      * stealth - stops navigator.webdriver
      
      from being accessible by the current web page. This is achieved by
      loading an extension
      ,
      which will automatically switch on the addons parameter for the new
      method. This is extremely experimental. See IMITATING OTHER BROWSERS
      for a discussion.

      * 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.

      * trackable - if this is set, profile preferences will be set to make
      it harder to be tracked by the browsers fingerprint
      
      across browser restarts. This is on by default, but may be switched
      off by setting it to 0;

      * 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 and
      NETWORK ARCHITECTURE. The user will default to the current user name.
      Authentication should be via public keys loaded into the local
      ssh-agent .

      * via - specifies a proxy jump box
       to be used to connect
      to a remote host. See the host parameter.

      * visible - should firefox be visible on the desktop. This defaults
      to "0". When moving from a X11 platform to another X11 platform, you
      can set visible to 'local' to enable X11 forwarding
      . See X11 FORWARDING WITH FIREFOX.

      * waterfox - only allow a binary that looks like a waterfox version
       to be launched.

      * webauthn - a boolean parameter to determine whether or not to add a
      webauthn authenticator after the connection is established. The
      default is to add a webauthn authenticator for Firefox after version
      118.

      * 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 => 'timestamp,nsHttp: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.
      shrink_to_fit should be disabled to make scale work.

      * 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);
                ...
        }

 percentage_visible

    accepts an element as the first parameter and returns the percentage of
    that element that is currently visible in the viewport
    . It
    achieves this by determining the co-ordinates of the DOMRect
     with a
    getBoundingClientRect
    
    call and then using elementsFromPoint
    
    and getComputedStyle
    
    calls to determine how the percentage of the DOMRect that is visible to
    the user. The getComputedStyle call is used to determine the state of
    the visibility
     and
    display 
    attributes.

        use Firefox::Marionette();
        use Encode();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new( visible => 1, kiosk => 1 )->go('http://metacpan.org');;
        my $element = $firefox->find_id('metacpan_search-input');
        my $totally_viewable_percentage = $firefox->percentage_visible($element); # search box is slightly hidden by different effects
        foreach my $display ($firefox->displays()) {
            if ($firefox->resize($display->width(), $display->height())) {
                if ($firefox->percentage_visible($element) < $totally_viewable_percentage) {
                   say 'Search box stops being fully viewable with ' . Encode::encode('UTF-8', $display->usage());
                   last;
                }
            }
        }

 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 from the HTML
    source code.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/');
        my $element = $firefox->find_id('metacpan_search-input');
        $element->property('value') eq '' or die "Initial property should be 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();

 resize

    accepts width and height parameters in a list and then attempts to
    resize the entire browser to match those parameters. Due to the
    oddities of various window managers, this function needs to manually
    calculate what the maximum and minimum sizes of the display is. It does
    this by;

    1 performing a maximise, then

    2 caching the browser's current width and height as the maximum width
    and height. It

    3 then calls resizeTo
     to
    resize the window to 0,0

    4 wait for the browser to send a resize
    
    event.

    5 cache the browser's current width and height as the minimum width and
    height

    6 if the requested width and height are outside of the maximum and
    minimum widths and heights return false

    7 if the requested width and height matches the current width and
    height return itself to aid in chaining methods. Otherwise,

    8 call resizeTo
     for
    the requested width and height

    9 wait for the resize
    
    event

    This method returns itself to aid in chaining methods if the method
    succeeds, otherwise it returns false.

        use Firefox::Marionette();
        use Encode();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new( visible => 1, kiosk => 1 )->go('http://metacpan.org');;
        if ($firefox->resize(1024, 768)) {
            say 'We are showing an XGA display';
        } else {
           say 'Resize failed to work';
        }

 resolve

    accepts a hostname as an argument and resolves it to a list of matching
    IP addresses. It can also accept an optional hash containing additional
    keys, described in Firefox::Marionette::DNS.

        use Firefox::Marionette();
        use v5.10;
    
        my $ssh_server = 'remote.example.org';
        my $firefox = Firefox::Marionette->new( host => $ssh_server );
        my $hostname = 'metacpan.org';
        foreach my $ip_address ($firefox->resolve($hostname)) {
           say "$hostname resolves to $ip_address at $ssh_server";
        }
        $firefox = Firefox::Marionette->new();
        foreach my $ip_address ($firefox->resolve($hostname, flags => Firefox::Marionette::DNS::RESOLVE_REFRESH_CACHE() | Firefox::Marionette::DNS::RESOLVE_BYPASS_CACHE(), type => Firefox::Marionette::DNS::RESOLVE_TYPE_DEFAULT())) {
           say "$hostname resolves to $ip_address;
        }

 resolve_override

    accepts a hostname and an IP address as parameters. This method then
    forces the browser to override any future DNS requests for the supplied
    hostname.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new();
        my $hostname = 'metacpan.org';
        my $ip_address = '127.0.0.1';
        foreach my $result ($firefox->resolve_override($hostname, $ip_address)->resolve($hostname)) {
           if ($result eq $ip_address) {
             warn "local metacpan time?";
           } else {
             die "This should not happen";
           }
        }
        $firefox->go('https://metacpan.org'); # this tries to contact a webserver on 127.0.0.1

    This method returns itself to aid in chaining methods.

 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 evaluated 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
      evaluated 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("metacpan_search-input")[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.

 scroll

    accepts a element as the first parameter and scrolls
    
    to it. The optional second parameter is the same as for the
    scrollInfoView method.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new(visible => 1)->go('https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView');
        my $link = $firefox->find_id('content')->find_link('Examples');
        $firefox->scroll($link);
        $firefox->scroll($link, 1);
        $firefox->scroll($link, { behavior => 'smooth', block => 'center' });
        $firefox->scroll($link, { block => 'end', inline => 'nearest' });

 send_alert_text

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

 set_javascript

    accepts a parameter for the the profile preference value of
    javascript.enabled
    .
    This method returns itself to aid in chaining methods.

 set_pref

    accepts a preference  name and
    the new value to set it to. See the get_pref and clear_pref methods to
    get a preference value and to restore it to it's original value. This
    method returns itself to aid in chaining methods.

        use Firefox::Marionette();
        my $firefox = Firefox::Marionette->new();
        ...
        $firefox->set_pref('browser.search.defaultenginename', 'DuckDuckGo');

 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();
        }

    See the FINDING ELEMENTS IN A SHADOW DOM section for how to delve into
    a shadow DOM
    .

 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 alternative 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 = $firefox->window_handle();
        $firefox->new_window( type => 'tab' );
        $firefox->new_window( type => 'window' );
        $firefox->switch_to_window($original_window);
        $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.

 tz

    accepts a Olson TZ identifier
     as
    the first parameter. This method returns itself to aid in chaining
    methods.

 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.

 uname

    returns the $^O ($OSNAME) compatible string to describe the platform
    where firefox is running.

 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

 webauthn_authenticator

    returns the default WebAuthn authenticator created when the new method
    was called.

 webauthn_credentials

    This method accepts an optional authenticator, in which case it will
    return all the credentials attached to this authenticator. If no
    parameter is supplied, credentials from the default authenticator will
    be returned.

        use Firefox::Marionette();
        use v5.10;
    
        my $firefox = Firefox::Marionette->new();
        foreach my $credential ($firefox->webauthn_credentials()) {
           say "Credential host is " . $credential->host();
        }
    
        # OR
    
        my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() );
        foreach my $credential ($firefox->webauthn_credentials($authenticator)) {
           say "Credential host is " . $credential->host();
        }

 webauthn_set_user_verified

    This method accepts a boolean for the is_user_verified field and an
    optional authenticator (the default authenticator will be used
    otherwise). It sets the is_user_verified field to the supplied boolean
    value.

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

 wheel

    accepts a element parameter, or a ( x => 0, y => 0 ) type hash manually
    describing exactly where to move the mouse from and returns an action
    for use in the perform method that corresponding with such a wheel
    action, 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.

      * deltaX - the change in X co-ordinates during the wheel. If not
      defined, deltaX defaults to 0.

      * deltaY - the change in Y co-ordinates during the wheel. If not
      defined, deltaY defaults to 0.

 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 existence 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. This is the same as the window
     object in
    Javascript.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new();
        my $original_window = $firefox->window_handle();
        my $javascript_window = $firefox->script('return window'); # only works for Firefox 121 and later
        if ($javascript_window ne $original_window) {
            die "That was unexpected!!! What happened?";
        }

 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 = $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

NETWORK ARCHITECTURE

    This module allows for a complicated network architecture, including
    SSH and HTTP proxies.

      my $firefox = Firefox::Marionette->new(
                      host  => 'Firefox.runs.here'
                      via   => 'SSH.Jump.Box',
                      trust => '/path/to/ca-for-squid-proxy-server.crt',
                      proxy => 'https://Squid.Proxy.Server:3128'
                         )->go('https://Target.Web.Site');

    produces the following effect, with an ascii box representing a
    separate network node.

         ---------          ----------         -----------
         | Perl  |  SSH     | SSH    |  SSH    | Firefox |
         | runs  |--------->| Jump   |-------->| runs    |
         | here  |          | Box    |         | here    |
         ---------          ----------         -----------
                                                    |
         ----------          ----------             |
         | Target |  HTTPS   | Squid  |    TLS      |
         | Web    |<---------| Proxy  |<-------------
         | Site   |          | Server |
         ----------          ----------

    In addition, the proxy parameter can be used to specify multiple
    proxies using a reference to a list.

      my $firefox = Firefox::Marionette->new(
                      host  => 'Firefox.runs.here'
                      trust => '/path/to/ca-for-squid-proxy-server.crt',
                      proxy => [ 'https://Squid1.Proxy.Server:3128', 'https://Squid2.Proxy.Server:3128' ]
                         )->go('https://Target.Web.Site');

    When firefox gets a list of proxies, it will use the first one that
    works. In addition, it will perform a basic form of proxy failover,
    which may involve a failed network request before it fails over to the
    next proxy. In the diagram below, Squid1.Proxy.Server is the first
    proxy in the list and will be used exclusively, unless it is
    unavailable, in which case Squid2.Proxy.Server will be used.

                                              ----------
                                         TLS  | Squid1 |
                                       ------>| Proxy  |-----
                                       |      | Server |    |
         ---------      -----------    |      ----------    |       -----------
         | Perl  | SSH  | Firefox |    |                    | HTTPS | Target  |
         | runs  |----->| runs    |----|                    ------->| Web     |
         | here  |      | here    |    |                    |       | Site    |
         ---------      -----------    |      ----------    |       -----------
                                       | TLS  | Squid2 |    |
                                       ------>| Proxy  |-----
                                              | Server |
                                              ----------

    See the REMOTE AUTOMATION OF FIREFOX VIA SSH section for more options.

    See SETTING UP SOCKS SERVERS USING SSH for easy proxying via ssh
    

    See GEO LOCATION section for how to combine this with providing
    appropriate browser settings for the end point.

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

GEO LOCATION

    The firefox Geolocation API
     can
    be used by supplying the geo parameter to the new method and then
    calling the geo method (from a secure context
    ).

    The geo method can accept various specific latitude and longitude
    parameters as a list, such as;

        $firefox->geo(latitude => -37.82896, longitude => 144.9811);
    
        OR
    
        $firefox->geo(lat => -37.82896, long => 144.9811);
    
        OR
    
        $firefox->geo(lat => -37.82896, lng => 144.9811);
    
        OR
    
        $firefox->geo(lat => -37.82896, lon => 144.9811);

    or it can be passed in as a reference, such as;

        $firefox->geo({ latitude => -37.82896, longitude => 144.9811 });

    the combination of a variety of parameter names and the ability to pass
    parameters in as a reference means it can be deal with various geo
    location websites, such as;

        $firefox->geo($firefox->json('https://freeipapi.com/api/json/')); # get geo location from current IP address
    
        $firefox->geo($firefox->json('https://geocode.maps.co/search?street=101+Collins+St&city=Melbourne&state=VIC&postalcode=3000&country=AU&format=json')->[0]); # get geo location of street address
    
        $firefox->geo($firefox->json('http://api.positionstack.com/v1/forward?access_key=' . $access_key . '&query=101+Collins+St,Melbourne,VIC+3000')->{data}->[0]); # get geo location of street address using api key
    
        $firefox->geo($firefox->json('https://api.ipgeolocation.io/ipgeo?apiKey=' . $api_key)); # get geo location from current IP address
    
        $firefox->geo($firefox->json('http://api.ipstack.com/142.250.70.206?access_key=' . $api_key)); # get geo location from specific IP address (http access only for free)

    These sites were active at the time this documentation was written, but
    mainly function as an illustration of the flexibility of geo and json
    methods in providing the desired location to the Geolocation API
    .

    As mentioned in the geo method documentation, the ipgeolocation API
     is the
    only API that currently providing geolocation data and matching
    timezone data in one API call. If this url is used, the tz method will
    be automatically called to set the timezone to the matching timezone
    for the geographic location.

CONSOLE LOGGING

    Sending debug to the console can be quite confusing in firefox, as some
    techniques won't work in chrome context. The following example can be
    quite useful.

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new( visible => 1, devtools => 1, console => 1, devtools => 1 );
    
        $firefox->script( q[console.log("This goes to devtools b/c it's being generated in content mode")]);
    
        $firefox->chrome()->script( q[console.log("Sent out on standard error for Firefox 136 and later")]);

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/');
    
        # OR use a proxy host to jump via to the final host
    
        my $firefox = Firefox::Marionette->new(
                                                 host  => 'remote.example.org',
                                                 port  => 2222,
                                                 via   => 'user@secure-jump-box.example.org:42222',
                                                 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.

    The module will expect to access private keys via the local ssh-agent
     when authenticating.

    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.

    The NETWORK ARCHITECTURE section has an example of a more complicated
    network design.

WEBGL

    There are a number of steps to getting WebGL
     to work correctly;

    1. The addons parameter to the new method must be set. This will
    disable -safe-mode
    

    2. The visible parameter to the new method must be set. This is due to
    an existing bug in Firefox
    .

    3. It can be tricky getting WebGL 
    to work with a Xvfb  instance.
    glxinfo  can be useful to
    help debug issues in this case. The mesa-dri-drivers rpm is also
    required for Redhat systems.

    With all those conditions being met, WebGL
     can be enabled like so;

        use Firefox::Marionette();
    
        my $firefox = Firefox::Marionette->new( addons => 1, visible => 1 );
        if ($firefox->script(q[let c = document.createElement('canvas'); return c.getContext('webgl2') ? true : c.getContext('experimental-webgl') ? true : false;])) {
            $firefox->go("https://get.webgl.org/");
        } else {
            die "WebGL is not supported";
        }

FINDING ELEMENTS IN A SHADOW DOM

    One aspect of Web Components
     is
    the shadow DOM
    .
    When you need to explore the structure of a custom element
    ,
    you need to access it via the shadow DOM. The following is an example
    of navigating the shadow DOM via a html file included in the test suite
    of this package.

        use Firefox::Marionette();
        use Cwd();
    
        my $firefox = Firefox::Marionette->new();
        my $firefox_marionette_directory = Cwd::cwd();
        $firefox->go("file://$firefox_marionette_directory/t/data/elements.html");
    
        my $shadow_root = $firefox->find_tag('custom-square')->shadow_root();
    
        my $outer_div = $firefox->find_id('outer-div', $shadow_root);

    So, this module is designed to allow you to navigate the shadow DOM
    using normal find methods, but you must get the shadow element's shadow
    root and use that as the root for the search into the shadow DOM. An
    important caveat is that xpath
     and tag name
     strategies do
    not officially work yet (and also the class name and name strategies).
    This module works around the tag name, class name and name deficiencies
    by using the matching css selector search if the original search throws
    a recognisable exception. Therefore these cases may be considered to be
    extremely experimental and subject to change when Firefox gets the
    "correct" functionality.

IMITATING OTHER BROWSERS

    There are a collection of methods and techniques that may be useful if
    you would like to change your geographic location or how the browser
    appears to your web site.

      * the stealth parameter of the new method. This method will stop the
      browser reporting itself as a robot and will also (when combined with
      the agent method, change other javascript characteristics to match
      the User Agent
      
      string.

      * the agent method, which if supplied a recognisable User Agent
      ,
      will attempt to change other attributes to match the desired browser.
      This is extremely experimental and feedback is welcome.

      * the geo method, which allows the modification of the Geolocation
      
      reported by the browser, but not the location produced by mapping the
      external IP address used by the browser (see the NETWORK ARCHITECTURE
      section for a discussion of different types of proxies that can be
      used to change your external IP address).

      * the languages method, which can change the requested languages
      
      for your browser session.

      * the tz method, which can change the timezone
      
      for your browser session.

    This list of methods may grow.

WEBSITES THAT BLOCK AUTOMATION

    Marionette by design
    
    allows web sites to detect that the browser is being automated. Firefox
    no longer (since version 88)
     allows you to
    disable this functionality while you are automating the browser, but
    this can be overridden with the stealth parameter for the new method.
    This is extremely experimental and feedback is welcome.

    If the web site you are trying to automate mysteriously fails when you
    are automating a workflow, but it works when you perform the workflow
    manually, you may be dealing with a web site that is hostile to
    automation. I would be very interested if you can supply a test case.

    At the very least, under these circumstances, it would be a good idea
    to be aware that there's an ongoing arms race
    ,
    and potential legal issues
     in this area.

X11 FORWARDING WITH FIREFOX

    X11 Forwarding  allows you to launch a
    remote firefox via ssh and have it visually appear in your local X11
    desktop. This can be accomplished with the following code;

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

    Feedback is welcome on any odd X11 workarounds that might be required
    for different platforms.

UBUNTU AND FIREFOX DELIVERED VIA SNAP

    Ubuntu 22.04 LTS
     is
    packaging firefox as a snap .
    This breaks the way that this module expects to be able to run,
    specifically, being able to setup a firefox profile in a systems
    temporary directory (/tmp or $TMPDIR in most Unix based systems) and
    allow the operating system to cleanup old directories caused by
    exceptions / network failures / etc.

    Because of this design decision, attempting to run a snap version of
    firefox will simply result in firefox hanging, unable to read it's
    custom profile directory and hence unable to read the marionette port
    configuration entry.

    Which would be workable except that; there does not appear to be _any_
    way to detect that a snap firefox will run (/usr/bin/firefox is a bash
    shell which eventually runs the snap firefox), so there is no way to
    know (heuristics aside) if a normal firefox or a snap firefox will be
    launched by execing 'firefox'.

    It seems the only way to fix this issue (as documented in more than a
    few websites) is;

    1. sudo snap remove firefox

    2. sudo add-apt-repository -y ppa:mozillateam/ppa

    3. sudo apt update

    4. sudo apt install -t 'o=LP-PPA-mozillateam' firefox

    5. echo -e "Package: firefox*\nPin: release
    o=LP-PPA-mozillateam\nPin-Priority: 501" >/tmp/mozillateamppa

    6. sudo mv /tmp/mozillateamppa /etc/apt/preferences.d/mozillateamppa

    If anyone is aware of a reliable method to detect if a snap firefox is
    going to launch vs a normal firefox, I would love to know about it.

    This technique is used in the setup-for-firefox-marionette-build.sh
    script in this distribution.

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

      * Time::Local

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();

    Also, check out the download method for an alternative.

 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 driver.js
      

      * about:config 

      * nsIPrefService interface
      

LICENSE AND COPYRIGHT

    Copyright (c) 2024, 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.63/ssh-auth-cmd-marionette0000755000175000017500000005430614763400572020202 0ustar  davedave#! /usr/bin/env -S perl -wT

use strict;
use warnings;
use Sys::Syslog();
use Fcntl();
use FileHandle();
use Symbol();
use Getopt::Long();
use English qw( -no_match_vars );
use File::Spec();

delete $ENV{PATH};
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};

my %os_paths = (
    freebsd   => '/usr/local/bin',
    dragonfly => '/usr/local/bin',
    netbsd    => '/usr/pkg/bin',
    openbsd   => '/usr/local/bin',
);
local $ENV{PATH} = '/usr/bin:/bin:/usr/sbin:/sbin'
  . (
    defined $os_paths{$OSNAME}
    ? q[:] . $os_paths{$OSNAME}
    : q[]
  );

our $VERSION = '1.63';

my $binary  = 'firefox';
my $ident   = 'ssh-auth-cmd-marionette';
my %options = ( facility => 'LOG_LOCAL0' );

sub _origin {
    my (
        $source_ip_address,      $source_port,
        $destination_ip_address, $destination_port
    ) = split q[ ], $ENV{SSH_CONNECTION};
    return $source_ip_address;
}

sub _tmp_directory_regex {
    my (%parameters) = @_;
    my $regex = qr{(?:/var)?/tmp}smx;
    if ( $OSNAME eq 'darwin' ) {    # getconf DARWIN_USER_TEMP_DIR
        my $handle    = FileHandle->new();
        my $command   = 'getconf';
        my @arguments = qw(DARWIN_USER_TEMP_DIR);
        if ( my $pid = $handle->open(q[-|]) ) {
            my $content;
            while ( my $line = <$handle> ) {
                $content .= $line;
            }
            close $handle
              or die "Failed to successfully complete:$EXTENDED_OS_ERROR\n";
            chomp $content;
            $content =~ s/\/$//smx;    # remove trailing / for darwin
            my $quoted_content = quotemeta $content;
            $regex = qr/$quoted_content/smx;
        }
        elsif ( defined $pid ) {
            eval {
                exec {$command} $command, @arguments
                  or die "Failed to exec '$command':$EXTENDED_OS_ERROR\n";
            } or do {
                chomp $EVAL_ERROR;
                Sys::Syslog::openlog( $ident, 'cons', $parameters{facility} );
                Sys::Syslog::syslog( Sys::Syslog::LOG_ERR(), $EVAL_ERROR );
                Sys::Syslog::closelog();
            };
            exit 1;
        }
        else {
            die "Failed to fork:$EXTENDED_OS_ERROR\n";
        }
    }
    return $regex;
}

eval {
    Getopt::Long::GetOptions(
        \%options,                          'help',
        'version',                          'facility:s',
        'allow-binary:s@',                  'force-binary:s',
        'regex-allow-binary-directories:s', 'regex-allow-binary-paths:s',
        'regex-allow-binary:s',             'scp-only',
    );
    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 $tmp_directory_regex = _tmp_directory_regex(%options);
    my $tmp_directory       = $ENV{TMPDIR} || '/tmp';
    $tmp_directory =~ s/\/$//smx;    # remove trailing / for darwin
    my $root_dir_regex;
    my $quoted_tmp_directory = quotemeta $tmp_directory;
    if ( $tmp_directory =~
s/^(${tmp_directory_regex}\/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/${$tmp_directory_regex}\/firefox_marionette_remote\w+/smx;
    }
    %options = _validate_parameters(%options);
    my ( $allowed_binary_directories_regex,
        $allowed_binary_paths_regex, $allowed_binary_regex )
      = _filesystem_regexes(%options);
    my $sub_directory_regex = qr/(?:profile|downloads|tmp|addons|certs)/smx;
    my $profile_names       = q[(?:] . (
        join q[|],
        map { quotemeta } (
            qw(
              bookmarks.html
              prefs.js
              mimeTypes.rdf
              search.json.mozlz4
            )
        )
    ) . q[)];
    my $profile_file_regex    = qr/profile\/$profile_names/smx;
    my $file_regex            = qr/[+\w\-()]{1,255}(?:[.][+\w\-()]{1,255})*/smx;
    my $downloads_regex       = qr/downloads\/$file_regex/smx;
    my $addons_regex          = qr/(?:addons|profile)\/$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 $xvfb_regex =
      qr/xvfb\-run[ ]\-a[ ]\-s[ ]"-screen[ ]0[ ]\d+x\d+x\d+"[ ]/smx;
    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/(?:[ ]\-\-jsconsole)?/smx,
      qr/(?:[ ]\-MOZ_LOG=[[:alnum:],:]+)?/smx,
      qr/(?:[ ]-safe\-mode)?/smx,
      qr/(?:[ ]\-headless)?/smx,
      qr/[ ](?:\-profile[ ]$root_dir_regex\/profile|\-P[ ][[:alnum:]]+)/smx,
      qr/(?:[ ]\-\-no\-remote)?/smx,
      qr/(?:[ ]\-\-new\-instance)?/smx,
      qr/(?:[ ]\-\-devtools)?/smx,
      qr/(?:[ ]\-\-kiosk)?/smx;
    my $prefs_grep_patterns_regex = join q[],
      qr/\-e[ ]marionette[ ]/smx,
      qr/\-e[ ]security[ ]/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 @sftp_server_regexs;
    if ( !$options{'scp-only'} ) {
        @sftp_server_regexs = (
            map { qr/${_}[ ]?/smx }
              ( # Adding a space for new sftp-server in Fedora 40 (openssh-9.6p1)
                quotemeta '/usr/libexec/openssh/sftp-server',   # Redhat, Debian
                quotemeta '/usr/libexec/sftp-server',           # FreeBSD
              )
        );
    }
    my $darwin_profile_regex =
      qr/Library\/Application\\[ ]Support\/Firefox\/Profiles/smx;
    my $named_profile_regex =
      qr/(?:[.]mozilla\/firefox|$darwin_profile_regex)/smx;
    my $profile_path_regex =
      qr/(?:$root_dir_regex\/profile|$named_profile_regex\/[[:alnum:].\-]+)/smx;
    my $scp_parameters_regex =
      qr/(?:[ ]\-v)?[ ]\-p[ ]\-[tf][ ](?:-P[ ]\d{2}[ ])?(?:\-v[ ])?/smx;
    my $quoted_linux_profiles_ini = quotemeta q[.mozilla/firefox];
    my $quoted_darwin_profiles_ini =
      quotemeta q[Library/Application Support/Firefox];
    my $profiles_ini_regex =
qr/(?:$quoted_linux_profiles_ini|$quoted_darwin_profiles_ini)\/profiles[.]ini/smx;
    my $allowed_commands_regex = join q[|],
      qr/"$allowed_binary_regex"[ ]\-\-version/smx,
      @darwin_regexes,
      @sftp_server_regexs,
      qr/uname[ ][|][|][ ]ver/smx,
      qr/echo[ ]"TMPDIR=\\"\$TMPDIR\\""/smx,
      qr/echo[ ]"DISPLAY=\\"\$DISPLAY\\""/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$scp_parameters_regex"?$root_dir_regex\/$profile_file_regex"?/smx,
      qr/scp(?:[ ]\-v)?[ ]\-p[ ]\-t[ ]"?$root_dir_regex\/$addons_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_regex\/$version_updates_regex/smx,
qr/scp(?:[ ]\-v)?[ ]\-p[ ]\-[tf][ ]"?$root_dir_regex\/$downloads_regex"?/smx,
      qr/scp[ ]\-p[ ]\-[tf][ ]"?$profiles_ini_regex"?/smx,
      qr/kill[ ]\-0[ ]\d{1,8}/smx,
      qr/which[ ]$allowed_binary_regex/smx,
      qr/readlink[ ]\-f[ ]$allowed_binary_paths_regex/smx,
qr/rm[ ]\-Rf[ ]$root_dir_regex(?:[ ]$quoted_tmp_directory\/Temp\-[\d\-a-f]{1,255})*/smx,
qr/ls[ ]-1[ ]"$allowed_binary_directories_regex(?:\/updates(?:\/\d+)?)?"/smx,
      qr/ls[ ]-1[ ]"$root_dir_regex\/downloads"/smx,
      qr/certutil$certutil_arguments_regex/smx,
      qr/(?:$xvfb_regex)?"$allowed_binary_regex"$firefox_arguments_regex/smx,
      qr/grep[ ]$prefs_grep_patterns_regex$profile_path_regex\/prefs[.]js/smx;

    my $user_name = getpwuid $EFFECTIVE_USER_ID;
    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' as '$user_name' from "
              . _origin() );
        Sys::Syslog::closelog();
        exec $command_and_arguments
          or die "Failed to '$command_and_arguments':$EXTENDED_OS_ERROR\n";
    }
    else {
        my $origin = _origin();
        Sys::Syslog::openlog( $ident, 'cons', $options{facility} );
        Sys::Syslog::syslog( Sys::Syslog::LOG_WARNING(),
                'Unrecognisable command "'
              . $ENV{SSH_ORIGINAL_COMMAND}
              . "\" as '$user_name' from $origin with a quoted TMPDIR of \"$quoted_tmp_directory\" and a root directory regex of \"$root_dir_regex\""
        );
        Sys::Syslog::closelog();
    }
    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();
};
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 _filesystem_regexes {
    my (%parameters) = @_;
    if (   ( $parameters{'regex-allow-binary-directories'} )
        && ( $parameters{'regex-allow-binary-paths'} )
        && ( $parameters{'regex-allow-binary'} ) )
    {
        my $allowed_binary_directories_regex;
        if ( $parameters{'regex-allow-binary-directories'} =~ /^(.*)$/smx ) {
            ($allowed_binary_directories_regex) = ($1);    # untaint
        }
        my $allowed_binary_paths_regex;
        if ( $parameters{'regex-allow-binary-paths'} =~ /^(.*)$/smx ) {
            ($allowed_binary_paths_regex) = ($1);          # untaint
        }
        my $allowed_binary_regex;
        if ( $parameters{'regex-allow-binary'} =~ /^(.*)$/smx ) {
            ($allowed_binary_regex) = ($1);                # untaint
        }
        return ( $allowed_binary_directories_regex,
            $allowed_binary_paths_regex, $allowed_binary_regex );
    }
    if ( $parameters{'regex-allow-binary-directories'} ) {
        Sys::Syslog::openlog( $ident, 'cons', $parameters{facility} );
        Sys::Syslog::syslog( Sys::Syslog::LOG_WARNING(),
'Ignoring --regex-allow-binary-directories parameter.  --regex-allow-binary-directories must be combined with --regex-allow-binary-paths and --regex-allow-binary'
        );
        Sys::Syslog::closelog();
    }
    if ( $parameters{'regex-allow-binary-paths'} ) {
        Sys::Syslog::openlog( $ident, 'cons', $parameters{facility} );
        Sys::Syslog::syslog( Sys::Syslog::LOG_WARNING(),
'Ignoring --regex-allow-binary-paths parameter.  --regex-allow-binary-paths must be combined with --regex-allow-binary-directories and --regex-allow-binary'
        );
        Sys::Syslog::closelog();
    }
    if ( $parameters{'regex-allow-binary'} ) {
        Sys::Syslog::openlog( $ident, 'cons', $parameters{facility} );
        Sys::Syslog::syslog( Sys::Syslog::LOG_WARNING(),
'Ignoring --regex-allow-binary parameter.  --regex-allow-binary must be combined with --regex-allow-binary-directories and --regex-allow-binary-paths'
        );
        Sys::Syslog::closelog();
    }
    my @allowed_binaries;
    my @allowed_binary_paths;
    my @allowed_binary_directories;
    foreach my $binary ( @{ $parameters{'allow-binary'} } ) {
        if ( $binary eq 'firefox' ) {
            push @allowed_binaries,           'firefox';
            push @allowed_binary_paths,       '/usr/bin/firefox';
            push @allowed_binary_directories, '/usr/bin';
            my %os_allowed_binary_paths = (
                freebsd   => '/usr/local/bin/firefox',
                dragonfly => '/usr/local/bin/firefox',
                netbsd    => '/usr/pkg/bin/firefox',
                openbsd   => '/usr/local/bin/firefox',
            );
            if ( $os_allowed_binary_paths{$OSNAME} ) {
                push @allowed_binary_paths, $os_allowed_binary_paths{$OSNAME};
            }
            my %os_allowed_binary_directories = (
                freebsd   => '/usr/local/lib/firefox',
                dragonfly => '/usr/local/lib/firefox',
                netbsd    => '/usr/pkg/bin',
                openbsd   => '/usr/local/lib/firefox',
            );
            if ( $os_allowed_binary_directories{$OSNAME} ) {
                push @allowed_binary_directories,
                  $os_allowed_binary_directories{$OSNAME};
            }
        }
        else {
            push @allowed_binaries,     $binary;
            push @allowed_binary_paths, $binary;
            my ( $volume, $directories ) = File::Spec->splitpath($binary);
            push @allowed_binary_directories,
              File::Spec->catdir( $volume, $directories );
            if ( $OSNAME eq 'darwin' ) {
                $directories =~ s{/Contents/MacOS/$}{/Contents/Resources/}smx;
                push @allowed_binary_directories,
                  File::Spec->catdir( $volume, $directories );
            }
        }
    }
    my $allowed_binaries =
      q[(?:] . ( join q[|], map { quotemeta } @allowed_binaries ) . q[)];
    my $allowed_binary_paths =
      q[(?:] . ( join q[|], map { quotemeta } @allowed_binary_paths ) . q[)];
    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_paths_regex       = qr/$allowed_binary_paths/smx;
    my $allowed_binary_regex             = qr/$allowed_binaries/smx;
    return ( $allowed_binary_directories_regex,
        $allowed_binary_paths_regex, $allowed_binary_regex );
}

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.63

=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.
 
=item * --scp-only - do not allow use of the SFTP protocol.  See the scp parameter in the L method.
 
=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

The following command will show syslog on OS X.

C

=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) 2024, 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.63/check-firefox-certificate-authorities0000755000175000017500000001466014763400566023075 0ustar  davedave#! /usr/bin/perl

use strict;
use warnings;
use Getopt::Long();
use English qw( -no_match_vars );
use Encode();
use Firefox::Marionette();

our $VERSION = '1.63';

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
      . Encode::encode( 'UTF-8', $certificate->subject_name(), 1 ) . "\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',
        'show-next' );
    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 $oldest;
    my $old_name;
    my $exit_code = 0;
    foreach my $certificate ( sort { $a->display_name() cmp $b->display_name }
        $firefox->certificates() )
    {
        if ( $certificate->is_ca_cert() ) {
            my $not_valid_after = $certificate->not_valid_after();
            if ( $not_valid_after < $now ) {
                warn Encode::encode( 'UTF-8', $certificate->display_name(), 1 )
                  . ' expired on '
                  . ( localtime $certificate->not_valid_after() ) . "\n";
                _write_common_output($certificate);
                $exit_code = 1;
            }
            elsif ( $certificate->not_valid_before > $now ) {
                warn Encode::encode( 'UTF-8', $certificate->display_name(), 1 )
                  . ' is not valid until '
                  . ( localtime $certificate->not_valid_before() ) . "\n";
                _write_common_output($certificate);
                $exit_code = 1;
            }
            elsif ( ( defined $oldest ) && ( $not_valid_after > $oldest ) ) {
            }
            else {
                $oldest   = $not_valid_after;
                $old_name = $certificate->display_name();
            }
        }
    }
    $firefox->quit();
    if ( $options{'show-next'} ) {
        print $old_name . ' will expire on ' . ( localtime $oldest ) . "\n"
          or die "Failed to print to STDOUT:$EXTENDED_OS_ERROR\n";
    }
    exit $exit_code;
}

__END__
=head1 NAME

check-firefox-certificate-authorities - check the CA certificates in firefox for expired certificates

=head1 VERSION

Version 1.63

=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

=item * --show-next - In addition to displaying any expired CA certificates, it will print out the next certificate that will expire and what date it will expire.

=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) 2024, 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.63/t/0000755000175000017500000000000014763402246014045 5ustar  davedaveFirefox-Marionette-1.63/t/data/0000755000175000017500000000000014763402246014756 5ustar  davedaveFirefox-Marionette-1.63/t/data/bookmarks_edge.html0000644000175000017500000001344314547652665020641 0ustar  davedave


Bookmarks

Bookmarks

Bookmarks bar

PAUSE: The CPAN back stage entrance

CPAN

Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org
Firefox-Marionette-1.38 - Automate the Firefox browser with the Marionette protocol - metacpan.org

Searches

perl 🫠at DuckDuckGo

GitHub - david-dick/firefox-marionette: This is a client module to automate the Mozilla Firefox browser via the Marionette protocol

PerlMonks - The Monastery Gates

Firefox-Marionette-1.63/t/data/1Passwordv7.csv0000644000175000017500000000116714547652665017654 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.63/t/data/bookmarks_firefox.json0000644000175000017500000000672114547652665021405 0ustar davedave{"guid":"root________","title":"","index":0,"dateAdded":1677537340939000,"lastModified":1686364095821000,"id":1,"typeCode":2,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"menu","index":0,"dateAdded":1677537340939000,"lastModified":1685672418372000,"id":2,"typeCode":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"ExAzkxs5efkR","title":"CPAN","index":0,"dateAdded":1685611344000000,"lastModified":1685672402832000,"id":19,"typeCode":2,"type":"text/x-moz-place-container","children":[{"guid":"utcxPgIOG05d","title":"Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org","index":0,"dateAdded":1685610972000000,"lastModified":1685610973000000,"id":20,"typeCode":1,"iconUri":"fake-favicon-uri:https://metacpan.org/pod/Firefox::Marionette","type":"text/x-moz-place","uri":"https://metacpan.org/pod/Firefox::Marionette"},{"guid":"XiZLcg8n_B4A","title":"Firefox-Marionette-1.38 - Automate the Firefox browser with the Marionette protocol - metacpan.org","index":1,"dateAdded":1685672252000000,"lastModified":1685672252000000,"id":21,"typeCode":1,"iconUri":"fake-favicon-uri:https://metacpan.org/release/DDICK/Firefox-Marionette-1.38","type":"text/x-moz-place","uri":"https://metacpan.org/release/DDICK/Firefox-Marionette-1.38"}]},{"guid":"eZlh-ztIYBne","title":"Searches","index":1,"dateAdded":1685670822000000,"lastModified":1685672383081000,"id":22,"typeCode":2,"type":"text/x-moz-place-container","children":[{"guid":"bZbedzEdrOnQ","title":"perl 🫠at DuckDuckGo","index":0,"dateAdded":1685670771000000,"lastModified":1685670772000000,"id":23,"typeCode":1,"tags":"perl,ðŸ«,UTF8,ddg","iconUri":"https://duckduckgo.com/favicon.ico","type":"text/x-moz-place","uri":"https://duckduckgo.com/?va=v&t=ha&q=perl+%F0%9F%90%AB&ia=web","keyword":"ðŸ«","postData":null}]},{"guid":"zCZX0_5lBXTS","title":"","index":2,"dateAdded":1685672418372000,"lastModified":1685672418372000,"id":24,"typeCode":3,"type":"text/x-moz-place-separator"},{"guid":"JTBNJdLPdYhk","title":"GitHub - david-dick/firefox-marionette: This is a client module to automate the Mozilla Firefox browser via the Marionette protocol","index":3,"dateAdded":1685611000000000,"lastModified":1685672392195000,"id":18,"typeCode":1,"iconUri":"fake-favicon-uri:https://github.com/david-dick/firefox-marionette","type":"text/x-moz-place","uri":"https://github.com/david-dick/firefox-marionette"}]},{"guid":"toolbar_____","title":"toolbar","index":1,"dateAdded":1677537340939000,"lastModified":1686364095821000,"id":3,"typeCode":2,"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"guid":"UjOiVVvuVdng","title":"PAUSE: The CPAN back stage entrance","index":0,"dateAdded":1686363942869000,"lastModified":1686363942869000,"id":26,"typeCode":1,"type":"text/x-moz-place","uri":"https://pause.perl.org/pause/query"}]},{"guid":"unfiled_____","title":"unfiled","index":3,"dateAdded":1677537340939000,"lastModified":1686364095821000,"id":5,"typeCode":2,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[{"guid":"wiTkT9Zqb99b","title":"PerlMonks - The Monastery Gates","index":0,"dateAdded":1686364081307000,"lastModified":1686364095821000,"id":27,"typeCode":1,"type":"text/x-moz-place","uri":"https://perlmonks.org/"}]},{"guid":"mobile______","title":"mobile","index":4,"dateAdded":1677537341411000,"lastModified":1677537341644000,"id":6,"typeCode":2,"type":"text/x-moz-place-container","root":"mobileFolder"}]} Firefox-Marionette-1.63/t/data/keepass.csv0000644000175000017500000000134314547652665017143 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.63/t/data/aria.html0000644000175000017500000000105614547652665016576 0ustar davedave ARIA tests

ARIA tests

Save
Firefox-Marionette-1.63/t/data/bookmarks_chrome.html0000644000175000017500000001344314547652665021212 0ustar davedave Bookmarks

Bookmarks

Bookmarks bar

PAUSE: The CPAN back stage entrance

CPAN

Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org
Firefox-Marionette-1.38 - Automate the Firefox browser with the Marionette protocol - metacpan.org

Searches

perl 🫠at DuckDuckGo

GitHub - david-dick/firefox-marionette: This is a client module to automate the Mozilla Firefox browser via the Marionette protocol

PerlMonks - The Monastery Gates

Firefox-Marionette-1.63/t/data/timezone.html0000644000175000017500000001224614633562513017503 0ustar davedave Timezone Tests

Firefox-Marionette-1.63/t/data/1Passwordv8.1pux0000644000175000017500000000702714547652665017760 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.63/t/data/bookmarks_firefox.html0000644000175000017500000002207614547652665021401 0ustar davedave Bookmarks

Bookmarks Menu

CPAN

Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org
Firefox-Marionette-1.38 - Automate the Firefox browser with the Marionette protocol - metacpan.org

Searches

perl 🫠at DuckDuckGo


GitHub - david-dick/firefox-marionette: This is a client module to automate the Mozilla Firefox browser via the Marionette protocol

Bookmarks Toolbar

PAUSE: The CPAN back stage entrance

Other Bookmarks

PerlMonks - The Monastery Gates

Firefox-Marionette-1.63/t/data/bookmarks_truncated.html0000644000175000017500000001307414547652665021726 0ustar davedave Bookmarks

Bookmarks Menu

CPAN

Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org
Firefox-Marionette-1.38 - Automate the Firefox browser with the Marionette protocol - metacpan.org Firefox-Marionette-1.63/t/data/bookmarks_empty.html0000644000175000017500000000063714547652665021074 0ustar davedave Bookmarks

Bookmarks Menu

Firefox-Marionette-1.63/t/data/keepassxs.csv0000644000175000017500000000061214547652665017514 0ustar davedave"Group","Title","Username","Password","URL","Notes","TOTP","Icon","Last Modified","Created" "Root","UTF-8 group","!""§$%&/()=?`´²³{[]}\","öüäÖÜÄß<>@€µ®“«","http://www.wébsite.org:8080","","","0","2022-02-09T09:27:03Z","2022-02-10T09:23:54Z" "Root","New Title","user-name","password","https://lwn.net","Here are the notes","","0","2022-02-11T09:36:13Z","2022-02-12T09:35:25Z" Firefox-Marionette-1.63/t/data/last_pass_example.csv0000644000175000017500000000112314547652665021210 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.63/t/data/logins.json0000644000175000017500000000120214547652665017153 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.63/t/data/bitwarden_export_org.csv0000644000175000017500000000072514547652665021742 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.63/t/data/visible.html0000644000175000017500000000273114547652665017320 0ustar davedave Percentage Visible test
Should not see me
Should not see me either
Should have collapsed
Content
Content
Firefox-Marionette-1.63/t/data/elements.html0000644000175000017500000000720214547652665017475 0ustar davedave Life cycle callbacks test

Elements

Firefox-Marionette-1.63/t/data/iframe.html0000644000175000017500000000024014547652665017117 0ustar davedave Iframe test

iframe

Firefox-Marionette-1.63/t/data/key4.db0000644000175000017500000110000014547652665016145 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.63/t/data/keepass1.xml0000644000175000017500000000566614547652665017245 0ustar davedave General Sample Entry Greg http://www.web.com sVoVd2HohmC7hpKYV5Bs This entry is stored in the 'General' group. 4d9a9420ac7c4a8ae688762eac8871a9 0 2006-12-31T11:52:01 2006-12-31T11:52:01 2999-12-28T23:59:59 Windows Sample Entry #2 michael@web.com L2shNNvQLmOWug68Rz2V This entry is stored in the 'Windows' subgroup of the 'General' group. bddda53fad29037bba34e7e42923c676 0 2006-12-31T11:52:38 2006-12-31T11:52:38 2999-12-28T23:59:59 Windows Expiring Entry me@website.org http://www.website.org USYbBOdTjaerrN/3l0VK This entry expires on 28.12.2008, at 23:59:59. c2674112d67f2787e822d845cde52858 0 2006-12-31T11:53:38 2006-12-31T11:53:38 2006-12-31T11:53:38 2008-12-28T23:59:59 Windows Special Characters Test !"§$%&/()=?`´²³{[]}\ http://www.website.org:8080 öüäÖÜÄß<>@€µ®“« The user name and password fields contain special characters. 6a4aae3134cb7b7d550d0bb7c98bc203 34 2006-12-31T11:55:57 2006-12-31T11:56:06 2006-12-31T11:56:06 2008-12-28T23:59:59 Windows Multi-Line Test user@website.com http://www.website.com v9ffIWkd/jPw/GPLFViW 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. 3d4fe2dc30d4ef0b62da6c7bf85e04ba 0 2006-12-31T11:56:49 2006-12-31T11:56:49 2006-12-31T11:56:49 2999-12-28T23:59:59 Firefox-Marionette-1.63/t/04-browserfeatcl.t0000644000175000017500000007140414704552721017322 0ustar davedave#! /usr/bin/perl -w use strict; use Firefox::Marionette(); use Test::More; use File::Spec(); use MIME::Base64(); use Socket(); use Config; use Crypt::URandom(); use lib qw(t/); $SIG{INT} = sub { die "Caught an INT signal"; }; $SIG{TERM} = sub { die "Caught a TERM signal"; }; my $min_stealth_version = 59; my $min_execute_script_with_null_args_version = 45; SKIP: { if (!$ENV{RELEASE_TESTING}) { plan skip_all => "Author tests not required for installation"; } if ($^O eq 'MSWin32') { plan skip_all => "Cannot test in a $^O environment"; } require test_daemons; if (!Test::Daemon::Nginx->available()) { plan skip_all => "nginx does not appear to be available"; } if ((Firefox::Marionette::BCD_PATH()) && (-f Firefox::Marionette::BCD_PATH())) { } else { plan skip_all => "BCD does not appear to be available. Please run build-bcd-for-firefox."; } my $nginx_listen = '127.0.0.1'; my $htdocs = File::Spec->catdir(Cwd::cwd(), 'browserfeatcl'); my $nginx = Test::Daemon::Nginx->new(listen => $nginx_listen, htdocs => $htdocs, index => 'index.html'); ok($nginx, "Started nginx Server on $nginx_listen on port " . $nginx->port() . ", with pid " . $nginx->pid()); $nginx->wait_until_port_open(); my $debug = $ENV{FIREFOX_DEBUG} || 0; my $visible = $ENV{FIREFOX_VISIBLE} || 0; my %extra_parameters; if ($ENV{FIREFOX_BINARY}) { $extra_parameters{binary} = $ENV{FIREFOX_BINARY}; } my $firefox = Firefox::Marionette->new( %extra_parameters, debug => $debug, visible => $visible, ); ok($firefox, "Created a normal firefox object"); my ($major_version, $minor_version, $patch_version) = split /[.]/smx, $firefox->browser_version(); my $original_agent = $firefox->agent(); ok($firefox->script('return navigator.webdriver') == JSON::true(), "\$firefox->script('return navigator.webdriver') returns true"); my $webdriver_prototype_call_script = 'try { Object.getPrototypeOf(navigator).webdriver } catch (e) { return e.toString() }'; my $original_webdriver_prototype_call_result = $firefox->script($webdriver_prototype_call_script); my $webdriver_prototype_call_regex = qr/TypeError:[ ]'get[ ]webdriver'[ ]called[ ]on[ ]an[ ]object[ ]that[ ]does[ ]not[ ]implement[ ]interface[ ]Navigator[.]/smx; ok($original_webdriver_prototype_call_result =~ /^$webdriver_prototype_call_regex$/smx, "window.Navigator.prototype.webdriver throws type error:" . $original_webdriver_prototype_call_result); my $webdriver_definition_script = 'return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(navigator), "webdriver").get.toString();'; my $original_webdriver_definition = $firefox->script($webdriver_definition_script); my $quoted_webdriver_definition = $original_webdriver_definition; $quoted_webdriver_definition =~ s/\n/\\n/smxg; my $webdriver_def_regex = qr/function[ ]webdriver[(][)][ ][{]\n?[ ]+\[native[ ]code\][\n ][}]/smx; ok($original_webdriver_definition =~ /^$webdriver_def_regex$/smx, "Webdriver definition matches regex:$quoted_webdriver_definition"); ok($firefox->quit() == 0, "\$firefox->quit() succeeded"); $firefox = Firefox::Marionette->new( %extra_parameters, debug => $debug, visible => $visible, stealth => 1, devtools => 1, ); ok($firefox, "Created a stealth firefox object"); check_webdriver($firefox, webdriver_definition_script => $webdriver_definition_script, webdriver_def_regex => $webdriver_def_regex, webdriver_prototype_call_script => $webdriver_prototype_call_script, webdriver_prototype_call_regex => $webdriver_prototype_call_regex, ); # checking against # https://browserleaks.com/javascript # https://www.amiunique.org/fingerprint # https://bot.sannysoft.com/ my $freebsd_118_user_agent_string = 'Mozilla/5.0 (X11; FreeBSD amd64; rv:109.0) Gecko/20100101 Firefox/118.0'; my %user_agents_to_js = ( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36' => { platform => 'Win32', appVersion => '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', productSub => '20030107', vendor => 'Google Inc.', vendorSub => '', oscpu => undef, }, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0' => { platform => 'Win32', appVersion => '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0', productSub => '20030107', vendor => 'Google Inc.', vendorSub => '', oscpu => undef, }, 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 OPR/109.0.0.0', { platform => 'Linux x86_64', appVersion => '5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 OPR/109.0.0.0', productSub => '20030107', vendor => 'Google Inc.', vendorSub => '', oscpu => undef, }, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15' => { platform => 'MacIntel', appVersion => '5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15', productSub => '20030107', vendor => 'Apple Computer, Inc.', vendorSub => '', oscpu => undef, }, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:109.0) Gecko/20100101 Firefox/115.0' => { platform => 'MacIntel', appVersion => '5.0 (Macintosh)', productSub => '20100101', vendor => '', vendorSub => '', oscpu => 'Intel Mac OS X 10.13', }, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0' => { platform => 'Win32', appVersion => '5.0 (Windows)', productSub => '20100101', vendor => '', vendorSub => '', oscpu => 'Win32', }, 'Mozilla/5.0 (X11; OpenBSD amd64; rv:109.0) Gecko/20100101 Firefox/109.0' => { platform => 'OpenBSD amd64', appVersion => '5.0 (X11)', productSub => '20100101', vendor => '', vendorSub => '', oscpu => 'OpenBSD amd64', }, 'Mozilla/5.0 (X11; NetBSD amd64; rv:120.0) Gecko/20100101 Firefox/120.0' => { platform => 'NetBSD amd64', appVersion => '5.0 (X11)', productSub => '20100101', vendor => '', vendorSub => '', oscpu => 'NetBSD amd64', }, 'Mozilla/5.0 (X11; Linux s390x; rv:109.0) Gecko/20100101 Firefox/115.0' => { platform => 'Linux s390x', appVersion => '5.0 (X11)', productSub => '20100101', vendor => '', vendorSub => '', oscpu => 'Linux s390x', }, 'Mozilla/5.0 (X11; DragonFly x86_64; rv:108.0) Gecko/20100101 Firefox/108.0' => { platform => 'DragonFly x86_64', appVersion => '5.0 (X11)', productSub => '20100101', vendor => '', vendorSub => '', oscpu => 'DragonFly x86_64', }, $freebsd_118_user_agent_string => { platform => 'FreeBSD amd64', appVersion => '5.0 (X11)', productSub => '20100101', vendor => '', vendorSub => '', oscpu => 'FreeBSD amd64', }, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0' => { platform => 'Win32', appVersion => '5.0 (Windows)', productSub => '20100101', vendor => '', vendorSub => '', oscpu => 'Win32', }, 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', { platform => 'iPhone', appVersion => '5.0 (iPhone; CPU iPhone OS 16_7_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', productSub => '20030107', vendor => 'Apple Computer, Inc.', vendorSub => '', oscpu => undef, }, 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36', { platform => 'Linux armv81', appVersion => '5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36', productSub => '20030107', vendor => 'Google Inc.', vendorSub => '', oscpu => undef, }, 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko', { platform => 'Win32', appVersion => '5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko', productSub => undef, vendor => '', vendorSub => undef, oscpu => undef, }, 'libwww-perl/6.72' => { }, ); foreach my $user_agent (sort { $a cmp $b } keys %user_agents_to_js) { if (($major_version < $min_execute_script_with_null_args_version) && (exists $user_agents_to_js{$user_agent}{oscpu}) && (!defined $user_agents_to_js{$user_agent}{oscpu})) { diag("Skipping '$user_agent' as oscpu will be null and executeScript cannot handle null arguments for older firefoxen"); next; } ok($user_agent, "Testing '$user_agent'"); ok($firefox->agent($user_agent), "\$firefox->agent(\"$user_agent\") succeeded"); if ($major_version > $min_stealth_version) { _check_navigator_attributes($firefox, $major_version, $user_agent, %user_agents_to_js); } ok($firefox->go("about:blank"), "\$firefox->go(\"about:blank\") loaded successfully for user agent test of values"); _check_navigator_attributes($firefox, $major_version, $user_agent, %user_agents_to_js); } if ($major_version > $min_stealth_version) { my $agent = $firefox->agent(undef); ok($agent, "\$firefox->agent(undef) should return 'libwww-perl/6.72'"); ok($agent eq 'libwww-perl/6.72', "\$firefox->agent(undef) did return '$agent'"); $firefox->set_javascript(0); ok(!$firefox->get_pref('javascript.enabled'), "Javascript is disabled for $agent"); $firefox->set_javascript(undef); ok($firefox->get_pref('javascript.enabled'), "Javascript is enabled for $agent"); $firefox->set_javascript(0); ok(!$firefox->get_pref('javascript.enabled'), "Javascript is disabled for $agent"); $firefox->set_javascript(1); ok($firefox->get_pref('javascript.enabled'), "Javascript is enabled for $agent"); $agent = $firefox->agent(version => 120); ok($agent eq $original_agent, "\$firefox->agent(version => 120) should return '$original_agent'"); ok($agent eq $original_agent, "\$firefox->agent(version => 120) did return '$agent'"); $agent = $firefox->agent(increment => -5); my $correct_agent = $original_agent; $correct_agent =~ s/rv:\d+/rv:120/smx; $correct_agent =~ s/Firefox\/\d+/Firefox\/120/smx; ok($correct_agent, "\$firefox->agent(increment => -5) should return '$correct_agent'"); ok($agent eq $correct_agent, "\$firefox->agent(increment => -5) did return '$agent'"); $agent = $firefox->agent(version => 108); $correct_agent = $original_agent; my $increment_major_version = $major_version - 5; my $increment_rv_version = $increment_major_version < 120 && $increment_major_version > 109 ? 109 : $increment_major_version; $correct_agent =~ s/rv:\d+/rv:$increment_rv_version/smx; $correct_agent =~ s/Firefox\/\d+/Firefox\/$increment_major_version/smx; ok($agent, "\$firefox->agent(version => 108) should return '$correct_agent'"); ok($agent eq $correct_agent, "\$firefox->agent(version => 108) did return '$agent'"); $agent = $firefox->agent(undef); $correct_agent = $original_agent; $correct_agent =~ s/rv:\d+/rv:108/smx; $correct_agent =~ s/Firefox\/\d+/Firefox\/108/smx; ok($agent, "\$firefox->agent(undef) should return '$correct_agent'"); ok($agent eq $correct_agent, "\$firefox->agent(undef) did return '$agent'"); $firefox->agent(os => 'Win64'); $agent = $firefox->agent(undef); ok($agent =~ /^Mozilla\/5[.]0[ ][(]Windows[ ]NT[ ]10[.]0;[ ]Win64;[ ]x64;[ ]rv:\d{2,3}[.]0[)][ ]Gecko\/20100101[ ]Firefox\/\d{2,3}[.]0$/smx, "\$firefox->agent(os => 'Win64') did return '$agent'"); $firefox->agent(os => 'FreeBSD', version => 110); $agent = $firefox->agent(undef); ok($agent =~ /^Mozilla\/5[.]0[ ][(]X11;[ ]FreeBSD[ ]amd64;[ ]rv:109.0[)][ ]Gecko\/20100101[ ]Firefox\/110.0$/smx, "\$firefox->agent(os => 'FreeBSD', version => 110) did return '$agent'"); $firefox->agent(os => 'linux', arch => 'i686'); $agent = $firefox->agent(undef); ok($agent =~ /^Mozilla\/5[.]0[ ][(]X11;[ ]Linux[ ]i686;[ ]rv:\d{2,3}.0[)][ ]Gecko\/20100101[ ]Firefox\/\d{2,3}.0$/smx, "\$firefox->agent(os => 'linux', arch => 'i686') did return '$agent'"); $firefox->agent(os => 'darwin'); $agent = $firefox->agent(undef); ok($agent =~ /^Mozilla\/5[.]0[ ][(]Macintosh;[ ]Intel[ ]Mac[ ]OS[ ]X[ ]\d+[.]\d+;[ ]rv:\d{2,3}.0[)][ ]Gecko\/20100101[ ]Firefox\/\d{2,3}.0$/smx, "\$firefox->agent(os => 'darwin') did return '$agent'"); $firefox->agent(os => 'darwin', platform => 'X11'); $agent = $firefox->agent(undef); ok($agent =~ /^Mozilla\/5[.]0[ ][(]X11;[ ]Intel[ ]Mac[ ]OS[ ]X[ ]\d+[.]\d+;[ ]rv:\d{2,3}.0[)][ ]Gecko\/20100101[ ]Firefox\/\d{2,3}.0$/smx, "\$firefox->agent(os => 'darwin', platform => 'X11') did return '$agent'"); $firefox->agent(os => 'darwin', arch => '10.13'); $agent = $firefox->agent(undef); ok($agent =~ /^Mozilla\/5[.]0[ ][(]Macintosh;[ ]Intel[ ]Mac[ ]OS[ ]X[ ]10[.]13;[ ]rv:\d{2,3}.0[)][ ]Gecko\/20100101[ ]Firefox\/\d{2,3}.0$/smx, "\$firefox->agent(os => 'darwin', arch => '10.13') did return '$agent'"); $firefox->agent(os => 'freebsd', version => 118); $agent = $firefox->agent(undef); ok($agent eq $freebsd_118_user_agent_string, "\$firefox->agent(os => 'freebsd', version => '118') did return '$agent'"); eval { $firefox->agent(version => 'blah') }; my $exception = $@; chomp $exception; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->agent(version => 'blah') throws an exception:$exception"); eval { $firefox->agent(increment => 'blah') }; $exception = $@; chomp $exception; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->agent(increment => 'blah') throws an exception:$exception"); } check_webdriver($firefox, webdriver_definition_script => $webdriver_definition_script, webdriver_def_regex => $webdriver_def_regex, webdriver_prototype_call_script => $webdriver_prototype_call_script, webdriver_prototype_call_regex => $webdriver_prototype_call_regex, ); { my $tmp_dir = File::Temp->newdir( TEMPLATE => File::Spec->catdir(File::Spec->tmpdir(), 'perl_ff_m_test_XXXXXXXXXXX') ) or die "Failed to create temporary directory:$!"; local $ENV{HOME} = $tmp_dir->dirname();; my $bcd_path = Firefox::Marionette::BCD_PATH(1); ok($bcd_path, "Created $bcd_path for BCD file in $bcd_path"); ok(1, "About to go to Firefox v122 with no BCD file available in $ENV{HOME}"); ok($firefox->agent(version => 122), "\$firefox->agent(version => 122) with no BCD file available, but BCD_PATH(1) called"); ok($firefox->agent(undef), "\$firefox->agent(undef) to reset agent string to original"); } { my $tmp_dir = File::Temp->newdir( TEMPLATE => File::Spec->catdir(File::Spec->tmpdir(), 'perl_ff_m_test_XXXXXXXXXXX') ) or die "Failed to create temporary directory:$!"; local $ENV{HOME} = $tmp_dir->dirname();; ok(1, "About to go to Firefox v122 with no BCD file available in $ENV{HOME}"); ok($firefox->agent(version => 122), "\$firefox->agent(version => 122) with no BCD file available and BCD_PATH(1) not called"); ok($firefox->agent(undef), "\$firefox->agent(undef) to reset agent string to original"); } { my %agent_parameters = ( from => 'Mozilla/5.0 (X11; Linux x86_64; rv:20.0) Gecko/20100101 Firefox/125.0', to => 'Mozilla/5.0 (X11; Linux x86_64; rv:20.0) Gecko/20100101 Firefox/100.0', filters => qr/(?:ContentVisibilityAutoStateChangeEvent|withResolvers)/smxi ); my $javascript = Firefox::Marionette::Extension::Stealth->user_agent_contents(%agent_parameters); ok($javascript =~ /delete[ ]window[.]ContentVisibilityAutoStateChangeEvent/, "Filtered extension code includes ContentVisibilityAutoStateChangeEvent"); ok($javascript !~ /delete[ ]window[.]ShadowRoot/, "Filtered extension code does NOT include ShadowRoot"); my $from = $agent_parameters{from}; my $to = $agent_parameters{to}; $agent_parameters{from} = $to; $agent_parameters{to} = $from; $javascript = Firefox::Marionette::Extension::Stealth->user_agent_contents(%agent_parameters); ok($javascript =~ /Object.defineProperty[(]window[.]ContentVisibilityAutoStateChangeEvent/, "Filtered extension code includes ContentVisibilityAutoStateChangeEvent"); ok($javascript !~ /Object.defineProperty[(]window[.]ShadowRoot/, "Filtered extension code does NOT include ShadowRoot"); $agent_parameters{from} = $from; $agent_parameters{to} = $to; delete $agent_parameters{filters}; $javascript = Firefox::Marionette::Extension::Stealth->user_agent_contents(%agent_parameters); ok($javascript =~ /delete[ ]window[.]ContentVisibilityAutoStateChangeEvent/, "Extension code includes ContentVisibilityAutoStateChangeEvent"); ok($javascript =~ /delete[ ]window[.]ShadowRoot/, "Extension code includes ShadowRoot"); } if ($ENV{FIREFOX_BCD}) { my $nightly_failures_found = 0; foreach my $version (reverse (6 .. 123)) { ok(1, "About to go to Firefox v$version"); my $agent = $firefox->agent(version => $version); ok($agent =~ /Firefox\/(\d+)/smx, "\$firefox->agent(version => $version) produces the actual agent string which contains Firefox version '$1'"); check_webdriver($firefox, webdriver_definition_script => $webdriver_definition_script, webdriver_def_regex => $webdriver_def_regex, webdriver_prototype_call_script => $webdriver_prototype_call_script, webdriver_prototype_call_regex => $webdriver_prototype_call_regex, ); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Loaded browserfeatcl"); $agent = $firefox->agent(); ok($agent =~ /Firefox\/(\d+)/smx, "\$firefox->agent() contains Firefox version '$1' in the agent string (real version is " . $firefox->browser_version() . ")"); my $test_result_version = $1; my $extracted_report = $firefox->await(sub { $firefox->has_class('success') or $firefox->has_class('error') })->text(); TODO: { local $TODO = $firefox->nightly() ? "Nightly releases may break feature detection from browser-compat-data" : q[]; ok($extracted_report =~ /You[']re[ ]using[ ]Firefox[ ](\d+)(?:[.]\d+)?(?:[ ]\-[ ](\d+)(?:[.]9)?[!])?/smx, "browserfeatcl reports '$extracted_report' which matches Firefox"); my ($min_version, $max_version) = ($1, $2); if (defined $max_version) { my $result; if ($min_version <= $version && $version <= $max_version) { $result = 1; } elsif ($firefox->nightly()) { $nightly_failures_found += 1; } ok($result, "browserfeatcl matches between $min_version and $max_version which includes fake version '$version'"); } elsif (defined $min_version) { my $result; if ($min_version == $version) { $result = 1; } elsif ($firefox->nightly()) { $nightly_failures_found += 1; } ok($result, "browserfeatcl matches $min_version which equals fake version '$version'"); } else { if ($firefox->nightly()) { $nightly_failures_found += 1; } ok(0, "browserfeatcl failed to output any version when looking for fake version '$version'"); } } check_webdriver($firefox, webdriver_definition_script => $webdriver_definition_script, webdriver_def_regex => $webdriver_def_regex, webdriver_prototype_call_script => $webdriver_prototype_call_script, webdriver_prototype_call_regex => $webdriver_prototype_call_regex, ); ok($firefox->agent(undef), "\$firefox->agent(undef) to reset agent string to original"); } foreach my $version (reverse (80 .. 121)) { my $chrome_user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$version.0.0.0 Safari/537.36"; ok(1, "About to go to Chrome v$version - $chrome_user_agent"); my $agent = $firefox->agent($chrome_user_agent); ok($agent =~ /Firefox\/(\d+)/smx, "\$firefox->agent('$chrome_user_agent') produces the actual agent string which contains Firefox version '$1'"); check_webdriver($firefox, webdriver_definition_script => $webdriver_definition_script, webdriver_def_regex => $webdriver_def_regex, webdriver_prototype_call_script => $webdriver_prototype_call_script, webdriver_prototype_call_regex => $webdriver_prototype_call_regex, ); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Loaded browserfeatcl"); $agent = $firefox->agent(); ok($agent =~ /Chrome\/$version/smx, "\$firefox->agent() contains Chrome version '$version' in the agent string (real version is Firefox v" . $firefox->browser_version() . ")"); my $extracted_report = $firefox->await(sub { $firefox->has_class('success') or $firefox->has_class('error') })->text(); TODO: { local $TODO = $firefox->nightly() ? "Nightly releases may break feature detection from browser-compat-data" : q[]; ok($extracted_report =~ /You[']re[ ]using[ ]Chrom(?:e|ium)[ ](\d+)(?:[.]\d+)?(?:[ ]\-[ ](\d+)[!])?/smx, "browserfeatcl reports '$extracted_report' which matches Chrome"); my ($min_version, $max_version) = ($1, $2); if (defined $max_version) { my $result; if ($min_version <= $version && $version <= $max_version) { $result = 1; } elsif ($firefox->nightly()) { $nightly_failures_found += 1; } ok($result, "browserfeatcl matches between $min_version and $max_version which includes fake version '$version'"); } elsif (defined $min_version) { my $result; if ($min_version == $version) { $result = 1; } elsif ($firefox->nightly()) { $nightly_failures_found += 1; } ok($result, "browserfeatcl matches $min_version which equals fake version '$version'"); } else { if ($firefox->nightly()) { $nightly_failures_found += 1; } ok(0, "browserfeatcl failed to output any version when looking for fake version '$version'"); } } check_webdriver($firefox, webdriver_definition_script => $webdriver_definition_script, webdriver_def_regex => $webdriver_def_regex, webdriver_prototype_call_script => $webdriver_prototype_call_script, webdriver_prototype_call_regex => $webdriver_prototype_call_regex, ); ok($firefox->agent(undef), "\$firefox->agent(undef) to reset agent string to original"); } foreach my $version (reverse (9 .. 17)) { my $safari_user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/$version Safari/605.1.15"; ok(1, "About to go to Safari v$version - $safari_user_agent"); my $agent = $firefox->agent($safari_user_agent); ok($agent =~ /Firefox\/(\d+)/smx, "\$firefox->agent('$safari_user_agent') produces the actual agent string which contains Firefox version '$1'"); check_webdriver($firefox, webdriver_definition_script => $webdriver_definition_script, webdriver_def_regex => $webdriver_def_regex, webdriver_prototype_call_script => $webdriver_prototype_call_script, webdriver_prototype_call_regex => $webdriver_prototype_call_regex, ); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Loaded browserfeatcl"); $agent = $firefox->agent(); ok($agent =~ /Version\/$version[ ]Safari\/(\d+)/smx, "\$firefox->agent() contains Safari version '$version' in the agent string (real version is Firefox v" . $firefox->browser_version() . ")"); my $extracted_report = $firefox->await(sub { $firefox->has_class('success') or $firefox->has_class('error') })->text(); TODO: { local $TODO = $firefox->nightly() ? "Nightly releases may break feature detection from browser-compat-data" : q[]; ok($extracted_report =~ /You[']re[ ]using[ ]Safari[ ](\d+)(?:[.]\d+)?(?:[ ]\-[ ](\d+)[.]9[!])?/smx, "browserfeatcl reports '$extracted_report' which matches Safari"); my ($min_version, $max_version) = ($1, $2); if (defined $max_version) { my $result; if ($min_version <= $version && $version <= $max_version) { $result = 1; } elsif ($firefox->nightly()) { $nightly_failures_found += 1; } ok($result, "browserfeatcl matches between $min_version and $max_version which includes fake version '$version'"); } elsif (defined $min_version) { my $result; if ($min_version == $version) { $result = 1; } elsif ($firefox->nightly()) { $nightly_failures_found += 1; } ok($result, "browserfeatcl matches $min_version which equals fake version '$version'"); } else { if ($firefox->nightly()) { $nightly_failures_found += 1; } ok(0, "browserfeatcl failed to output any version when looking for fake version '$version'"); } } check_webdriver($firefox, webdriver_definition_script => $webdriver_definition_script, webdriver_def_regex => $webdriver_def_regex, webdriver_prototype_call_script => $webdriver_prototype_call_script, webdriver_prototype_call_regex => $webdriver_prototype_call_regex, ); ok($firefox->agent(undef), "\$firefox->agent(undef) to reset agent string to original"); } if ($nightly_failures_found) { diag("$nightly_failures_found nightly failures have been found"); } } ok($firefox->quit() == 0, "\$firefox->quit() was successful()"); ok($nginx->stop() == 0, "Stopped nginx on $nginx_listen:" . $nginx->port()); } sub check_webdriver { my ($firefox, %parameters) = @_; if ($firefox->script(q[if ('webdriver' in navigator) { return 1 } else { return 0 }])) { eval { ok($firefox->script('return navigator.webdriver') == JSON::false(), "\$firefox->script('return navigator.webdriver') returns false"); my $stealth_webdriver_definition = $firefox->script($parameters{webdriver_definition_script}); my $quoted_webdriver_definition = $stealth_webdriver_definition; $quoted_webdriver_definition =~ s/\n/\\n/smxg; ok($stealth_webdriver_definition =~ /^$parameters{webdriver_def_regex}$/smx, "Webdriver definition matches:$quoted_webdriver_definition"); my $stealth_webdriver_prototype_call_result = $firefox->script($parameters{webdriver_prototype_call_script}); ok(defined $stealth_webdriver_prototype_call_result && $stealth_webdriver_prototype_call_result =~ /^$parameters{webdriver_prototype_call_regex}$/smx, "window.Navigator.prototype.webdriver throws type error:" . ($stealth_webdriver_prototype_call_result || "no exception thrown at all")); }; } else { my $agent = $firefox->agent(); ok(1, "Webdriver does not exist for " . $agent); } return; } sub _check_navigator_attributes { my ($firefox, $major_version, $user_agent, %user_agents_to_js) = @_; my $count = 0; KEY: foreach my $key (qw( platform appVersion )) { my $value = $firefox->script('return navigator.' . $key); if ($user_agent =~ /^libwww[-]perl/smx) { ok(defined $value, "navigator.$key is unchanged as '$value'"); } elsif (defined $user_agents_to_js{$user_agent}{$key}) { if (($value ne $user_agents_to_js{$user_agent}{$key}) && ($major_version < 62) && ($major_version > 59) && ($count <= 1)) { # firefox-60.0esr has blown up on this b/c of a seeming race condition my $redo_seconds = 4; $count += 1; diag("The navigator.$key value is incorrect as '$value'. Waiting $redo_seconds seconds to try again"); sleep $redo_seconds; redo KEY; } ok($value eq $user_agents_to_js{$user_agent}{$key}, "navigator.$key is now '$user_agents_to_js{$user_agent}{$key}':$value"); } else { ok(!defined $value, "navigator.$key is undefined"); } } if ($major_version > $min_stealth_version) { $count = 0; KEY2: foreach my $key (qw( productSub vendor vendorSub oscpu )) { my $value = $firefox->script('return navigator.' . $key); if ($user_agent =~ /^libwww[-]perl/smx) { ok(defined $value, "navigator.$key is unchanged as '$value'"); } elsif (defined $user_agents_to_js{$user_agent}{$key}) { if (($value ne $user_agents_to_js{$user_agent}{$key}) && ($major_version < 62) && ($major_version > 59) && ($count <= 1)) { # firefox-60.0esr has blown up on this b/c of a seeming race condition my $redo_seconds = 4; $count += 1; diag("The navigator.$key value is incorrect as '$value'. Waiting $redo_seconds seconds to try again"); sleep $redo_seconds; redo KEY2; } ok($value eq $user_agents_to_js{$user_agent}{$key}, "navigator.$key is now '$user_agents_to_js{$user_agent}{$key}':$value"); } else { ok(!defined $value, "navigator.$key is undefined"); } } } my $value = $firefox->script('return navigator.userAgent'); ok($user_agent eq $value, "navigator.userAgent is now '$user_agent':$value"); } done_testing(); Firefox-Marionette-1.63/t/03-close.t0000644000175000017500000000074514547652665015601 0ustar davedave#! /usr/bin/perl -w use strict; use JSON(); use IPC::Open3(); use Archive::Zip(); use XML::Parser(); use lib qw(t/); use syscall_tests (qw(close)); *CORE::GLOBAL::close = sub { if (syscall_tests::allow()) { CORE::close $_[0]; } else { $! = POSIX::EIO(); return } }; require Firefox::Marionette; syscall_tests::run(POSIX::EIO()); syscall_tests::visible(POSIX::EIO()); no warnings; *CORE::GLOBAL::close = sub { return CORE::close $_[0]; }; use warnings; syscall_tests::finalise(); Firefox-Marionette-1.63/t/test_daemons.pm0000644000175000017500000011350514633562513017075 0ustar davedavepackage Test::File::Temp; use strict; use warnings; use Carp(); use English qw( -no_match_vars ); use File::Spec(); use File::Temp(); use Crypt::PasswdMD5(); sub tmp_handle { my ( $class, $name ) = @_; my $handle = File::Temp->new( UNLINK => 1, TEMPLATE => File::Spec->catfile( File::Spec->tmpdir(), 'firefox_marionette_test_daemon_file_' . $name . '_XXXXXXXXXXX' ) ) or Carp::croak( "Failed to open temporary file for writing:$EXTENDED_OS_ERROR"); fcntl $handle, Fcntl::F_SETFD(), 0 or Carp::croak( "Failed to clear the close-on-exec flag on a temporary file:$EXTENDED_OS_ERROR" ); return $handle; } sub tmp_directory { my ( $class, $name ) = @_; my $directory = File::Temp->newdir( CLEANUP => 1, TEMPLATE => File::Spec->catdir( File::Spec->tmpdir(), 'firefox_marionette_test_daemon_directory_' . $name . '_XXXXXXXXXXX' ) ) or Carp::croak("Failed to create temporary directory:$EXTENDED_OS_ERROR"); return $directory; } package Test::Binary::Available; use strict; use warnings; use Carp(); use English qw( -no_match_vars ); use File::Spec(); sub find_binary { my ( $class, $binary ) = @_; foreach my $directory ( split /:/smx, $ENV{PATH} ) { my $possible_path = File::Spec->catfile( $directory, $binary ); if ( -e $possible_path ) { return $possible_path; } } return $binary; } sub available { my ( $class, $binary, @arguments ) = @_; my $debug; if ( $ENV{FIREFOX_DEBUG} ) { $debug = $ENV{FIREFOX_DEBUG}; } my $dev_null = File::Spec->devnull(); if ( my $pid = fork ) { waitpid $pid, 0; } else { eval { open STDOUT, q[>], $dev_null or Carp::croak( "Failed to redirect STDOUT to $dev_null:$EXTENDED_OS_ERROR"); if ( !$debug ) { open STDERR, q[>], $dev_null or Carp::croak( "Failed to redirect STDERR to $dev_null:$EXTENDED_OS_ERROR" ); } open STDIN, q[<], $dev_null or Carp::croak( "Failed to redirect STDIN to $dev_null:$EXTENDED_OS_ERROR"); exec {$binary} $binary, @arguments; } or do { # absolutely nothing, this is allowed to fail }; exit 1; } return $CHILD_ERROR == 0 ? 1 : 0; } package Test::CA; use strict; use warnings; use English qw( -no_match_vars ); @Test::CA::ISA = qw(Test::Binary::Available Test::File::Temp); my $openssl_binary = 'openssl'; sub available { my ($class) = @_; return $class->SUPER::available( $openssl_binary, 'version' ); } sub new { my ( $class, $key_size ) = @_; my $self = bless {}, $class; $self->{ca_directory} = $class->tmp_directory('ca'); $self->{ca_cert_path} = File::Spec->catfile( $self->{ca_directory}->dirname(), 'ca.crt' ); $self->{ca_cert_handle} = FileHandle->new( $self->{ca_cert_path}, Fcntl::O_EXCL() | Fcntl::O_RDWR() | Fcntl::O_CREAT(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Carp::croak("Failed to create $self->{ca_cert_path}:$EXTENDED_OS_ERROR"); $self->{ca_private_key_path} = File::Spec->catfile( $self->{ca_directory}->dirname(), 'ca.key' ); $self->{ca_private_key_handle} = $class->new_key( $key_size, $self->{ca_private_key_path} ); $self->{ca_serial_path} = File::Spec->catfile( $self->{ca_directory}->dirname(), 'ca.serial' ); $self->{ca_serial_handle} = FileHandle->new( $self->{ca_serial_path}, Fcntl::O_EXCL() | Fcntl::O_RDWR() | Fcntl::O_CREAT(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Carp::croak( "Failed to create $self->{ca_serial_path}:$EXTENDED_OS_ERROR"); print { $self->{ca_serial_handle} } '01' or Carp::croak( "Failed to write to $self->{ca_serial_path}:$EXTENDED_OS_ERROR"); close $self->{ca_serial_handle} or Carp::croak("Failed to close $self->{ca_serial_path}:$EXTENDED_OS_ERROR"); $self->{ca_config_path} = File::Spec->catfile( $self->{ca_directory}->dirname(), 'ca.config' ); $self->{ca_config_handle} = FileHandle->new( $self->{ca_config_path}, Fcntl::O_EXCL() | Fcntl::O_RDWR() | Fcntl::O_CREAT(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Carp::croak( "Failed to create $self->{ca_config_path}:$EXTENDED_OS_ERROR"); $self->{ca_config_handle}->print(<<"_CONFIG_"); [ req ] distinguished_name = req_distinguished_name x509_extensions = v3_ca 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 ] [ signing_policy ] countryName = optional stateOrProvinceName = optional localityName = optional organizationName = optional organizationalUnitName = optional commonName = supplied emailAddress = optional #################################################################### [ signing_req ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer basicConstraints = CA:FALSE keyUsage = digitalSignature, keyEncipherment prompt = no [ v3_ca ] keyUsage=critical, keyCertSign subjectKeyIdentifier=hash authorityKeyIdentifier=keyid:always,issuer:always basicConstraints=critical,CA:TRUE,pathlen:1 extendedKeyUsage=serverAuth _CONFIG_ seek $self->{ca_config_handle}, 0, 0 or Carp::croak( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); system {$openssl_binary} $openssl_binary, 'req', '-new', '-x509', '-config' => $self->{ca_config_path}, '-days' => 10, '-key' => $self->{ca_private_key_path}, '-out' => $self->{ca_cert_path} and Carp::croak( "Failed to generate a CA root certificate:$EXTENDED_OS_ERROR"); return $self; } sub config { my ($self) = @_; return $self->{ca_config_path}; } sub serial { my ($self) = @_; return $self->{ca_serial_path}; } sub cert { my ($self) = @_; return $self->{ca_cert_path}; } sub key { my ($self) = @_; return $self->{ca_private_key_path}; } sub new_cert { my ( $self, $key_path, $host_name, $path ) = @_; my $csr = $self->tmp_handle('csr'); my $cert_handle; my $cert_path; if ($path) { $cert_handle = FileHandle->new( $path, Fcntl::O_EXCL() | Fcntl::O_RDWR() | Fcntl::O_CREAT(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Carp::croak("Failed to create $path:$EXTENDED_OS_ERROR"); $cert_path = $path; } else { $cert_handle = $self->tmp_handle('cert'); $cert_path = $cert_handle->filename(); } system {$openssl_binary} $openssl_binary, 'req', '-new', '-sha256', '-config' => $self->config(), '-key' => $key_path, '-subj' => "/C=AU/ST=Victoria/L=Melbourne/O=David Dick/OU=CPAN/CN=$host_name", '-out' => $csr->filename() and Carp::croak( "Failed to generate a certificate signing request:$EXTENDED_OS_ERROR"); my $cert_extensions_handle = $self->tmp_handle('cert_ext'); $cert_extensions_handle->print(<<"_CONFIG_"); authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, keyEncipherment subjectAltName = \@alt_names [alt_names] IP.1 = $host_name _CONFIG_ seek $cert_extensions_handle, 0, 0 or Carp::croak( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); system {$openssl_binary} $openssl_binary, 'x509', '-req', '-in' => $csr->filename(), '-CA' => $self->cert(), '-CAkey' => $self->key(), '-extfile' => $cert_extensions_handle->filename(), '-CAserial' => $self->serial(), '-sha256', '-days' => 10, '-out' => $cert_path and Carp::croak("Failed to generate a certificate:$EXTENDED_OS_ERROR"); my $ca_cert = FileHandle->new( $self->cert(), Fcntl::O_RDONLY() ) or Carp::croak( 'Failed to open ' . $self->cert() . " for reading:$EXTENDED_OS_ERROR" ); seek $cert_handle, 0, Fcntl::SEEK_END() or Carp::croak( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); while ( my $line = <$ca_cert> ) { print {$cert_handle} $line or Carp::croak("Failed to write to temporary file:$EXTENDED_OS_ERROR"); } seek $cert_handle, 0, Fcntl::SEEK_SET() or Carp::croak( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); return $cert_handle; } sub new_key { my ( $class, $size, $path ) = @_; my $private_key_handle; my $private_key_path; if ($path) { $private_key_handle = FileHandle->new( $path, Fcntl::O_EXCL() | Fcntl::O_RDWR() | Fcntl::O_CREAT(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Carp::croak("Failed to create $path:$EXTENDED_OS_ERROR"); $private_key_path = $path; } else { $private_key_handle = $class->tmp_handle('private_key'); $private_key_path = $private_key_handle->filename(); } system {$openssl_binary} $openssl_binary, 'genrsa', '-out' => $private_key_path, $size and Carp::croak("Failed to generate a private key:$EXTENDED_OS_ERROR"); return $private_key_handle; } package Test::Daemon; use strict; use warnings; use Carp(); use Config; use Socket(); use English qw( -no_match_vars ); @Test::Daemon::ISA = qw(Test::File::Temp); sub CONVERT_TO_PROCESS_GROUP { return -1 } 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; } sub new { my ( $class, %parameters ) = @_; my $debug = delete $parameters{debug}; if ( $ENV{FIREFOX_DEBUG} ) { $debug = $ENV{FIREFOX_DEBUG}; } my @arguments = @{ delete $parameters{arguments} }; my %extra = %parameters; my $self = bless { binary => $parameters{binary}, arguments => \@arguments, port => $parameters{port}, debug => $debug, %extra }, $class; $self->start(); return $self; } sub debug { my ($self) = @_; return $self->{debug}; } sub arguments { my ($self) = @_; return @{ $self->{arguments} }; } sub address { my ($self) = @_; return $self->{listen}; } sub wait_until_port_open { my ($self) = @_; my $address = $self->address(); my $port = $self->port(); my $found_port = 0; while ( $found_port == 0 ) { socket my $socket, Socket::PF_INET(), Socket::SOCK_STREAM(), 0 or Carp::croak("Failed to create a socket:$EXTENDED_OS_ERROR"); my $sock_addr = Socket::pack_sockaddr_in( $port, Socket::inet_aton($address) ); if ( connect $socket, $sock_addr ) { $found_port = $port; } else { my $kid = waitpid $self->pid(), POSIX::WNOHANG(); if ( $kid == $self->pid() ) { Carp::croak('Server died while waiting for port to open'); } sleep 1; } close $socket or Carp::croak("Failed to close test socket:$EXTENDED_OS_ERROR"); } return; } sub directory { my ($self) = @_; return $self->{directory}; } sub start { my ($self) = @_; my $dev_null = File::Spec->devnull(); if ( $self->{pid} = fork ) { return $self->{pid}; } elsif ( defined $self->{pid} ) { eval { local $SIG{INT} = 'DEFAULT'; local $SIG{TERM} = 'DEFAULT'; if ( $self->{resetpg} ) { setpgrp $PID, 0 or Carp::croak( "Failed to reset process group:$EXTENDED_OS_ERROR"); } if ( my $directory = $self->directory() ) { chdir $directory or Carp::croak("Failed to chdir $directory:$EXTENDED_OS_ERROR"); } open STDOUT, q[>], $dev_null or Carp::croak( "Failed to redirect STDOUT to $dev_null:$EXTENDED_OS_ERROR"); if ( !$self->debug() ) { open STDERR, q[>], $dev_null or Carp::croak( "Failed to redirect STDERR to $dev_null:$EXTENDED_OS_ERROR" ); } open STDIN, q[<], $dev_null or Carp::croak( "Failed to redirect STDIN to $dev_null:$EXTENDED_OS_ERROR"); exec { $self->{binary} } $self->{binary}, $self->arguments() or Carp::croak( "Failed to exec '$self->{binary}':$EXTENDED_OS_ERROR"); } or do { Carp::carp($EVAL_ERROR); }; exit 1; } return; } sub pid { my ($self) = @_; return $self->{pid}; } sub port { my ($self) = @_; return $self->{port}; } sub new_port { socket my $socket, Socket::PF_INET(), Socket::SOCK_STREAM(), 0 or Carp::croak("Failed to create a socket:$EXTENDED_OS_ERROR"); bind $socket, Socket::sockaddr_in( 0, Socket::INADDR_LOOPBACK() ) or Carp::croak("Failed to bind socket:$EXTENDED_OS_ERROR"); my $port = ( Socket::sockaddr_in( getsockname $socket ) )[0]; close $socket or Carp::croak("Failed to close random socket:$EXTENDED_OS_ERROR"); return $port; } sub stop { my ($self) = @_; if ( my $pid = $self->{pid} ) { kill $signals_by_name{TERM}, $pid; waitpid $pid, 0; delete $self->{pid}; return $CHILD_ERROR; } return; } sub stop_process_group { my ($self) = @_; if ( my $pid = $self->{pid} ) { my $pgrp = getpgrp $self->{pid} or Carp::croak( "Failed to get process group from $self->{pid}:$EXTENDED_OS_ERROR"); $pgrp *= CONVERT_TO_PROCESS_GROUP(); kill $signals_by_name{INT}, $pgrp; my $kid = waitpid $pgrp, 0; while ( $kid > 0 ) { sleep 1; $kid = waitpid $pgrp, POSIX::WNOHANG(); if ( $kid > 0 ) { Carp::carp("Also gathered $kid"); } } delete $self->{pid}; return 0; } return; } sub DESTROY { my ($self) = @_; if ( $self->{resetpg} ) { $self->stop_process_group(); } if ( my $pid = delete $self->{pid} ) { while ( kill 0, $pid ) { kill $signals_by_name{TERM}, $pid; sleep 1; waitpid $pid, POSIX::WNOHANG(); } } return; } package Test::Daemon::Nginx; use strict; use warnings; use Carp(); use Crypt::URandom(); use English qw( -no_match_vars ); @Test::Daemon::Nginx::ISA = qw(Test::Daemon Test::Binary::Available); sub _RANDOM_STRING_LENGTH { return 50 } my $nginx_binary = __PACKAGE__->find_binary('nginx'); sub available { my ($class) = @_; return $class->SUPER::available( $nginx_binary, '-v' ); } sub write_passwd { my ( $class, $passwd_path, $username, $password ) = @_; if ( $username || $password ) { my $passwd_handle = FileHandle->new( $passwd_path, Fcntl::O_WRONLY() | Fcntl::O_EXCL() | Fcntl::O_CREAT(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Carp::croak("Failed to open $passwd_path:$EXTENDED_OS_ERROR"); my $encrypted_password = Crypt::PasswdMD5::unix_md5_crypt($password); print {$passwd_handle} "$username:$encrypted_password\n" or Carp::croak("Failed to write to $passwd_path:$EXTENDED_OS_ERROR"); close $passwd_handle or Carp::croak("Failed to close $passwd_path:$EXTENDED_OS_ERROR"); } return; } sub new { my ( $class, %parameters ) = @_; my $listen = $parameters{listen}; my $key_size = $parameters{key_size}; my $ca = $parameters{ca}; my $username = $parameters{username}; my $password = $parameters{password}; my $realm = $parameters{realm}; my $root_name = $parameters{htdocs}; my $index_name = $parameters{index}; my $port = $class->new_port(); my $base_directory = $class->tmp_directory('nginx'); my $passwd_path = File::Spec->catfile( $base_directory->dirname(), 'htpasswd' ); $class->write_passwd( $passwd_path, $username, $password ); my $key_path = File::Spec->catfile( $base_directory->dirname(), 'nginx.key' ); my $certificate_path = File::Spec->catfile( $base_directory->dirname(), 'nginx.crt' ); if ( $key_size && $ca ) { my $key_handle = $ca->new_key( $key_size, $key_path ); my $certificate_handle = $ca->new_cert( $key_path, $listen, $certificate_path ); } my $random_string; if ( !$root_name ) { $root_name = 'htdocs'; my $root_directory = File::Spec->catfile( $base_directory->dirname(), $root_name ); mkdir $root_directory, Fcntl::S_IRWXU() or Carp::croak("Failed to mkdir $root_directory:$EXTENDED_OS_ERROR"); if ( !$index_name ) { $index_name = 'index.txt'; my $index_file_path = File::Spec->catfile( $root_directory, $index_name ); my $index_handle = FileHandle->new( $index_file_path, Fcntl::O_WRONLY() | Fcntl::O_EXCL() | Fcntl::O_CREAT(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Carp::croak("Failed to open $index_file_path:$EXTENDED_OS_ERROR"); $random_string = MIME::Base64::encode_base64( Crypt::URandom::urandom( _RANDOM_STRING_LENGTH() ) ); chomp $random_string; print {$index_handle} $random_string or Carp::croak( "Failed to write to $index_file_path:$EXTENDED_OS_ERROR"); close $index_handle or Carp::croak( "Failed to close $index_file_path:$EXTENDED_OS_ERROR"); } } my $pid_path = File::Spec->catfile( $base_directory->dirname(), 'nginx.pid' ); my $pid_handle = FileHandle->new( $pid_path, Fcntl::O_WRONLY() | Fcntl::O_EXCL() | Fcntl::O_CREAT(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Carp::croak("Failed to open $pid_path:$EXTENDED_OS_ERROR"); my $config_path = File::Spec->catfile( $base_directory->dirname(), 'nginx.conf' ); my $log_directory = File::Spec->catdir( $base_directory->dirname(), 'logs' ); mkdir $log_directory, Fcntl::S_IRWXU() or Carp::croak("Failed to mkdir $log_directory:$EXTENDED_OS_ERROR"); my $error_log_path = File::Spec->catfile( $log_directory, 'error.log' ); my $access_log_path = File::Spec->catfile( $base_directory->dirname(), 'access.log' ); my $config_handle = FileHandle->new( $config_path, Fcntl::O_WRONLY() | Fcntl::O_EXCL() | Fcntl::O_CREAT(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Carp::croak("Failed to open $config_path:$EXTENDED_OS_ERROR"); my %temp_directories; foreach my $name ( qw(client_body_temp proxy_temp fastcgi_temp uwsgi_temp scgi_temp)) { $temp_directories{$name} = File::Spec->catfile( $base_directory->dirname(), $name ); mkdir $temp_directories{$name}, Fcntl::S_IRWXU() or Carp::croak( "Failed to mkdir $temp_directories{$name}:$EXTENDED_OS_ERROR"); } print {$config_handle} <<"_NGINX_CONF_" or Carp::croak("Failed to write to temporary file:$EXTENDED_OS_ERROR"); daemon off; pid $pid_path; error_log logs/error.log; events { worker_connections 1024; } http { client_body_temp_path $temp_directories{client_body_temp}; proxy_temp_path $temp_directories{proxy_temp}; fastcgi_temp_path $temp_directories{fastcgi_temp}; uwsgi_temp_path $temp_directories{uwsgi_temp}; scgi_temp_path $temp_directories{scgi_temp}; access_log logs/access.log; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 4096; types { text/html html; text/javascript js; text/css css; application/json json; } default_type text/plain; server { _NGINX_CONF_ if ( $key_size && $ca ) { print {$config_handle} <<"_NGINX_CONF_" or Carp::croak("Failed to write to temporary file:$EXTENDED_OS_ERROR"); listen $listen:$port ssl; _NGINX_CONF_ } else { print {$config_handle} <<"_NGINX_CONF_" or Carp::croak("Failed to write to temporary file:$EXTENDED_OS_ERROR"); listen $listen:$port; _NGINX_CONF_ } print {$config_handle} <<"_NGINX_CONF_" or Carp::croak("Failed to write to temporary file:$EXTENDED_OS_ERROR"); server_name default; _NGINX_CONF_ if ( $username || $password ) { print {$config_handle} <<"_NGINX_CONF_" or Carp::croak("Failed to write to temporary file:$EXTENDED_OS_ERROR"); auth_basic "$realm"; auth_basic_user_file $passwd_path; _NGINX_CONF_ } if ( $key_size && $ca ) { print {$config_handle} <<"_NGINX_CONF_" or Carp::croak("Failed to write to temporary file:$EXTENDED_OS_ERROR"); ssl_certificate $certificate_path; ssl_certificate_key $key_path; ssl_protocols TLSv1.2; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:!AES128-SHA:!DES-CBC3-SHA:!MD5:!aNULL:!CAMELLIA:!PSK:!SRP; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_stapling off; ssl_stapling_verify off; ssl_ecdh_curve secp384r1; _NGINX_CONF_ } print {$config_handle} <<"_NGINX_CONF_" or Carp::croak("Failed to write to temporary file:$EXTENDED_OS_ERROR"); server_tokens off; root $root_name; index $index_name; } } _NGINX_CONF_ close $config_handle or Carp::croak("Failed to close $config_path:$EXTENDED_OS_ERROR"); my $nginx = $class->SUPER::new( debug => $parameters{debug}, binary => $nginx_binary, pid_handle => $pid_handle, listen => $listen, port => $port, base_directory => $base_directory, content => $random_string, arguments => [ qw(-c), $config_path, qw(-p), $base_directory ] ); return $nginx; } sub content { my ($self) = @_; return $self->{content}; } package Test::Daemon::Squid; use strict; use warnings; use Carp(); use English qw( -no_match_vars ); @Test::Daemon::Squid::ISA = qw(Test::Daemon Test::Binary::Available); my $squid_binary = __PACKAGE__->find_binary('squid'); sub available { my ($class) = @_; return $class->SUPER::available( $squid_binary, '--version' ); } sub find_basic_ncsa_auth { my $basic_ncsa_auth_path; foreach my $possible_path ( '/usr/lib64/squid/basic_ncsa_auth', # Redhat, Fedora '/usr/lib/squid/basic_ncsa_auth', # Alpine Linux, Debian '/usr/local/libexec/squid/basic_ncsa_auth' , # FreeBSD, DragonflyBSD, OpenBSD '/usr/pkg/libexec/basic_ncsa_auth', # NetBSD ) { if ( -e $possible_path ) { $basic_ncsa_auth_path = $possible_path; last; } } return $basic_ncsa_auth_path; } sub new { my ( $class, %parameters ) = @_; my $listen = $parameters{listen}; my $username = $parameters{username}; my $password = $parameters{password}; my $realm = $parameters{realm}; my $key_size = $parameters{key_size}; my $ca = $parameters{ca}; my $port = $class->new_port(); my $base_directory = $class->tmp_directory('squid'); my $basic_ncsa_auth_path = $class->find_basic_ncsa_auth(); my $passwd_path = File::Spec->catfile( $base_directory->dirname(), 'htpasswd' ); if ( $username || $password ) { my $passwd_handle = FileHandle->new( $passwd_path, Fcntl::O_WRONLY() | Fcntl::O_EXCL() | Fcntl::O_CREAT(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Carp::croak("Failed to open $passwd_path:$EXTENDED_OS_ERROR"); my $encrypted_password = Crypt::PasswdMD5::unix_md5_crypt($password); print {$passwd_handle} "$username:$encrypted_password\n" or Carp::croak("Failed to write to $passwd_path:$EXTENDED_OS_ERROR"); close $passwd_handle or Carp::croak("Failed to close $passwd_path:$EXTENDED_OS_ERROR"); } my $key_path = File::Spec->catfile( $base_directory->dirname(), 'squid.key' ); my $certificate_path = File::Spec->catfile( $base_directory->dirname(), 'squid.crt' ); if ( $key_size && $ca ) { my $key_handle = $ca->new_key( $key_size, $key_path ); my $certificate_handle = $ca->new_cert( $key_path, $listen, $certificate_path ); } my $config_path = File::Spec->catfile( $base_directory->dirname(), 'squid.config' ); my $config_handle = FileHandle->new( $config_path, Fcntl::O_WRONLY() | Fcntl::O_EXCL() | Fcntl::O_CREAT(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR() ) or Carp::croak("Failed to open $config_path:$EXTENDED_OS_ERROR"); if ( $username || $password ) { print {$config_handle} <<"_SQUID_CONF_" or Carp::croak("Failed to write to $config_path:$EXTENDED_OS_ERROR"); auth_param basic program $basic_ncsa_auth_path $passwd_path auth_param basic realm $realm auth_param basic casesensitive on acl Auth proxy_auth REQUIRED _SQUID_CONF_ } if ( $parameters{allow_ssl_port} ) { print {$config_handle} <<"_SQUID_CONF_" or Carp::croak("Failed to write to $config_path:$EXTENDED_OS_ERROR"); acl SSL_ports port $parameters{allow_ssl_port} http_access deny !SSL_ports http_access deny CONNECT !SSL_ports _SQUID_CONF_ } elsif ( $parameters{allow_port} ) { print {$config_handle} <<"_SQUID_CONF_" or Carp::croak("Failed to write to $config_path:$EXTENDED_OS_ERROR"); acl HTTP_ports port $parameters{allow_port} http_access deny !HTTP_ports http_access deny CONNECT HTTP_ports _SQUID_CONF_ } if ( $username || $password ) { print {$config_handle} <<"_SQUID_CONF_" or Carp::croak("Failed to write to $config_path:$EXTENDED_OS_ERROR"); http_access deny !Auth _SQUID_CONF_ } print {$config_handle} <<"_SQUID_CONF_" or Carp::croak("Failed to write to $config_path:$EXTENDED_OS_ERROR"); http_access allow localhost _SQUID_CONF_ if ( $key_size && $ca ) { print {$config_handle} <<"_SQUID_CONF_" or Carp::croak("Failed to write to $config_path:$EXTENDED_OS_ERROR"); https_port $listen:$port tls-cert=$certificate_path tls-key=$key_path _SQUID_CONF_ } else { print {$config_handle} <<"_SQUID_CONF_" or Carp::croak("Failed to write to $config_path:$EXTENDED_OS_ERROR"); http_port $listen:$port _SQUID_CONF_ } print {$config_handle} <<"_SQUID_CONF_" or Carp::croak("Failed to write to $config_path:$EXTENDED_OS_ERROR"); shutdown_lifetime 0 seconds visible_hostname $listen pid_filename none access_log /dev/stderr cache_log /dev/null _SQUID_CONF_ close $config_handle or Carp::croak("Failed to close $config_path:$EXTENDED_OS_ERROR"); my $squid = $class->SUPER::new( debug => $parameters{debug}, binary => $squid_binary, listen => $listen, base_directory => $base_directory, port => $port, arguments => [ qw(-f), $config_path, qw(-N -d 3) ] ); return $squid; } package Test::Daemon::SSH; use strict; use warnings; use Carp(); use English qw( -no_match_vars ); @Test::Daemon::SSH::ISA = qw(Test::Daemon Test::Binary::Available); sub _DEFAULT_PORT { return 22 } my $sshd_binary = __PACKAGE__->find_binary('sshd'); sub _sshd_config { my ( $class, %parameters ) = @_; my $listen = $parameters{listen}; my $port = $parameters{port}; my $key_handle = $parameters{key_handle}; my $key_path = $key_handle->filename(); my $config_handle = $class->tmp_handle('sshd_config'); my $config_path = $config_handle->filename(); print {$config_handle} <<"_SSHD_CONF_" or Carp::croak("Failed to write to temporary file:$EXTENDED_OS_ERROR"); HostKey $key_path ListenAddress $listen Port $port _SSHD_CONF_ seek $config_handle, 0, 0 or Carp::croak( "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR"); return $config_handle; } sub available { my ( $class, %parameters ) = @_; my $listen = $parameters{listen}; my $key_size = $parameters{key_size}; my $port = $class->new_port(); my $ca = $parameters{ca}; my $key_handle = $ca->new_key($key_size); my $config_handle = $class->_sshd_config( key_handle => $key_handle, listen => $listen, port => $port ); my $config_path = $config_handle->filename(); return $class->SUPER::available( $sshd_binary, '-e', '-t', '-f', $config_path ); } sub new { my ( $class, %parameters ) = @_; my $listen = $parameters{listen}; my $key_size = $parameters{key_size}; my $ca = $parameters{ca}; my $port = _DEFAULT_PORT(); my $key_handle = $ca->new_key($key_size); my $config_handle = $class->_sshd_config( key_handle => $key_handle, listen => $listen, port => $port ); my $config_path = $config_handle->filename(); my $ssh = $class->SUPER::new( debug => $parameters{debug}, binary => $sshd_binary, listen => $listen, port => $port, key_handle => $key_handle, config_handle => $config_handle, arguments => [ qw(-D -e -f), $config_path ] ); return $ssh; } sub connect_and_exit { my ( $class, $host ) = @_; my $binary = 'ssh'; if ( !$class->SUPER::available( $binary, '-o', 'ConnectTimeout=5', '-o', 'BatchMode=yes', $host, 'exit 0' ) ) { return 0; } my $port = $class->new_port(); my $result = system {$binary} $binary, '-o', 'ConnectTimeout=5', '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'ExitOnForwardFailure=yes', '-L', "$port:127.0.0.1:22", $host, 'exit 0'; return $result == 0 ? return 1 : return $result; } package Test::Daemon::Socks; use strict; use warnings; use Carp(); use English qw( -no_match_vars ); @Test::Daemon::Socks::ISA = qw(Test::Daemon Test::Binary::Available); my $ssh_binary = __PACKAGE__->find_binary('ssh'); sub available { my ( $class, %parameters ) = @_; my $listen = $parameters{listen}; my $port = $class->new_port(); my $config_handle = $class->_sshd_config( listen => $listen, port => $port ); my $config_path = $config_handle->filename(); return $class->SUPER::available( $ssh_binary, '-V' ); } sub new { my ( $class, %parameters ) = @_; my $listen = $parameters{listen}; my $port = $class->new_port(); my $ssh = $class->SUPER::new( debug => $parameters{debug}, binary => $ssh_binary, listen => $listen, port => $port, arguments => [ qw(-o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ExitOnForwardFailure=yes -ND), "$listen:$port", 'localhost' ] ); return $ssh; } package Test::Daemon::Botd; use strict; use warnings; use Cwd(); use Carp(); use English qw( -no_match_vars ); @Test::Daemon::Botd::ISA = qw(Test::Daemon Test::Binary::Available); my $yarn_binary = __PACKAGE__->find_binary('yarnpkg') || __PACKAGE__->find_binary('yarn'); sub botd_available { my $cwd; if ( ( Cwd::cwd() =~ /^(.*)$/smx ) && ( -d File::Spec->catdir( $1, 'BotD' ) ) ) { return 1; } return; } sub available { my ($class) = @_; return $class->SUPER::available( $yarn_binary, '--version' ); } sub new { my ( $class, %parameters ) = @_; my $listen = $parameters{listen}; my $port = $class->new_port(); my $cwd; if ( Cwd::cwd() =~ /^(.*)$/smx ) { $cwd = $1; } else { Carp::croak(q[Unable to untaint current working directory]); } my $git_repo_dir = File::Spec->catdir( $cwd, '.git' ); if ( -d $git_repo_dir ) { system {'git'} 'git', 'submodule', 'init' and Carp::croak("Failed to 'git submodule init':$EXTENDED_OS_ERROR"); system {'git'} 'git', 'submodule', 'update', '--remote' and Carp::croak("Failed to 'git submodule update':$EXTENDED_OS_ERROR"); } my $botd_directory = File::Spec->catdir( $cwd, 'BotD' ); my $dev_null = File::Spec->devnull(); if ( my $pid = fork ) { waitpid $pid, 0; } elsif ( defined $pid ) { eval { local $SIG{INT} = 'DEFAULT'; local $SIG{TERM} = 'DEFAULT'; chdir $botd_directory or Carp::croak("Failed to chdir $botd_directory:$EXTENDED_OS_ERROR"); open STDOUT, q[>], $dev_null or Carp::croak( "Failed to redirect STDOUT to $dev_null:$EXTENDED_OS_ERROR"); if ( !$parameters{debug} ) { open STDERR, q[>], $dev_null or Carp::croak( "Failed to redirect STDERR to $dev_null:$EXTENDED_OS_ERROR" ); } open STDIN, q[<], $dev_null or Carp::croak( "Failed to redirect STDIN to $dev_null:$EXTENDED_OS_ERROR"); exec {$yarn_binary} $yarn_binary, 'install' or Carp::croak("Failed to exec '$yarn_binary':$EXTENDED_OS_ERROR"); } or do { Carp::carp($EVAL_ERROR); }; exit 1; } else { Carp::croak("Failed to fork:$EXTENDED_OS_ERROR"); } my $botd = $class->SUPER::new( debug => $parameters{debug}, binary => $yarn_binary, directory => $botd_directory, listen => $listen, resetpg => 1, port => $port, arguments => [ q[dev:playground], q[--host], $listen, q[--port], $port ], ); return $botd; } sub stop { my ($self) = @_; return $self->stop_process_group(); } package Test::Daemon::FingerprintJS; use strict; use warnings; use Cwd(); use Carp(); use English qw( -no_match_vars ); @Test::Daemon::FingerprintJS::ISA = qw(Test::Daemon Test::Binary::Available); sub fingerprintjs_available { my $cwd; if ( ( Cwd::cwd() =~ /^(.*)$/smx ) && ( -d File::Spec->catdir( $1, 'fingerprintjs' ) ) ) { return 1; } return; } sub available { my ($class) = @_; return $class->SUPER::available( $yarn_binary, '--version' ); } sub new { my ( $class, %parameters ) = @_; my $listen = $parameters{listen}; my $port = $class->new_port(); my $cwd; if ( Cwd::cwd() =~ /^(.*)$/smx ) { $cwd = $1; } else { Carp::croak(q[Unable to untaint current working directory]); } my $git_repo_dir = File::Spec->catdir( $cwd, '.git' ); if ( -d $git_repo_dir ) { system {'git'} 'git', 'submodule', 'init' and Carp::croak("Failed to 'git submodule init':$EXTENDED_OS_ERROR"); system {'git'} 'git', 'submodule', 'update', '--remote' and Carp::croak("Failed to 'git submodule update':$EXTENDED_OS_ERROR"); } my $fingerprintjs_directory = File::Spec->catdir( $cwd, 'fingerprintjs' ); my $dev_null = File::Spec->devnull(); if ( my $pid = fork ) { waitpid $pid, 0; } elsif ( defined $pid ) { eval { local $SIG{INT} = 'DEFAULT'; local $SIG{TERM} = 'DEFAULT'; chdir $fingerprintjs_directory or Carp::croak( "Failed to chdir $fingerprintjs_directory:$EXTENDED_OS_ERROR"); open STDOUT, q[>], $dev_null or Carp::croak( "Failed to redirect STDOUT to $dev_null:$EXTENDED_OS_ERROR"); if ( !$parameters{debug} ) { open STDERR, q[>], $dev_null or Carp::croak( "Failed to redirect STDERR to $dev_null:$EXTENDED_OS_ERROR" ); } open STDIN, q[<], $dev_null or Carp::croak( "Failed to redirect STDIN to $dev_null:$EXTENDED_OS_ERROR"); exec {$yarn_binary} $yarn_binary, 'install' or Carp::croak("Failed to exec '$yarn_binary':$EXTENDED_OS_ERROR"); } or do { Carp::carp($EVAL_ERROR); }; exit 1; } else { Carp::croak("Failed to fork:$EXTENDED_OS_ERROR"); } my $fingerprintjs = $class->SUPER::new( debug => $parameters{debug}, binary => $yarn_binary, directory => $fingerprintjs_directory, listen => $listen, resetpg => 1, port => $port, arguments => [ q[playground:start], q[--host], $listen, q[--port], $port ], ); return $fingerprintjs; } sub stop { my ($self) = @_; return $self->stop_process_group(); } 1; Firefox-Marionette-1.63/t/author/0000755000175000017500000000000014763402246015347 5ustar davedaveFirefox-Marionette-1.63/t/author/bulk_test.pl0000755000175000017500000012772614640242476017723 0ustar davedave#! /usr/bin/perl use strict; use warnings; use DirHandle(); use File::HomeDir(); use File::Spec(); use File::Temp(); use FileHandle(); use IO::Socket(); use English qw( -no_match_vars ); use Text::CSV_XS(); use Cwd(); use POSIX(); use Config; my $oldfh = select STDOUT; $OUTPUT_AUTOFLUSH = 1; select $oldfh; $oldfh = select STDERR; $OUTPUT_AUTOFLUSH = 1; select $oldfh; my @sig_names = split q[ ], $Config{sig_name}; our $start_time = time; print "Start time is " . (localtime $start_time) . "\n"; if (exists $ENV{COUNT}) { $0 = "Test run number $ENV{COUNT}"; } system { 'ssh-add' } 'ssh-add', '-l' and die "The SSH agent needs to be loaded with keys"; my $background_failed; my $parent_pid = $PID; my $devel_cover_db_format = 'JSON'; my $cover_db_path = '/tmp'; my $cover_db_name = 'cover_db'; my $devel_cover_inc = '-MDevel::Cover=-silent,1'; my $devel_cover_inc_with_space = $devel_cover_inc ? " $devel_cover_inc" : q[]; my $local_devel_cover_inc = $devel_cover_inc ? "$devel_cover_inc,-db,$cover_db_path/$cover_db_name" : q[]; my $local_devel_cover_inc_with_space = $local_devel_cover_inc ? " $local_devel_cover_inc" : q[]; my $test_marionette_file = 't/01-marionette.t'; my $reset_time = 600; # 10 minutes my $max_attempts = 3; my $reboot_sleep_time = 60; $ENV{RELEASE_TESTING} = 1; $ENV{FIREFOX_ALARM} = 900; $ENV{DEVEL_COVER_DB_FORMAT} = $devel_cover_db_format; system { 'cover' } 'cover', '-delete', $cover_db_name and die "Failed to 'cover' for " . ($ENV{FIREFOX_BINARY} || 'firefox'); system { 'cover' } 'cover', '-delete', "$cover_db_path/$cover_db_name" and die "Failed to 'cover' for " . ($ENV{FIREFOX_BINARY} || 'firefox'); MAIN: { my $cwd = Cwd::cwd(); my $test_directory = File::Spec->catdir($cwd, 't'); my $test_directory_handle = DirHandle->new($test_directory); my @syscall_entries; if ($test_directory_handle) { while(my $entry = $test_directory_handle->read()) { next if ($entry eq File::Spec->updir()); next if ($entry eq File::Spec->curdir()); next if ($entry !~ /03\-/smx); push @syscall_entries, File::Spec->catfile($test_directory, $entry); } } else { die "Failed to open test directory:$!"; } my @servers; my $csv = Text::CSV_XS->new ({ binary => 1, auto_diag => 1 }); my $servers_path = $cwd . '/servers.csv'; if (open my $handle, "<:encoding(utf8)", $servers_path) { my %headers; my $count = 0; foreach my $name ($csv->header($handle, { detect_bom => 1, munge_column_names => sub { lc (s/[ ]/_/grsmx ) }})) { $headers{$name} = $count; $count += 1; } while (my $row = $csv->getline ($handle)) { my $server = {}; foreach my $key (sort { $a cmp $b } keys %headers) { $server->{$key} = $row->[$headers{$key}]; } $server->{port} ||= $server->{os} eq 'android' ? 5555 : 22; $server->{user} ||= getpwuid $UID; push @servers, $server; } close $handle or die "Failed to close $servers_path:$EXTENDED_OS_ERROR"; } elsif ($OS_ERROR == POSIX::ENOENT()) { } else { die "Failed to open $servers_path for reading: $EXTENDED_OS_ERROR"; } my $win32_remote_alarm = 7800; my $win32_via_alarm = 7800; my $background_pids = {}; foreach my $server (@servers) { if (my $pid = fork) { $background_pids->{$pid} = $server; } elsif (defined $pid) { eval { my $win32_local_alarm = 1200; my $cygwin_local_alarm = 2700; my $cygwin_remote_alarm = 7800; my $physical_local_alarm = 1200; $ENV{FIREFOX_ALARM} = $win32_remote_alarm; $ENV{FIREFOX_NO_RECONNECT} = 1; if ((lc $server->{type}) eq 'virsh') { if (_virsh_node_running($server)) { _determine_address($server); _virsh_shutdown($server); _sleep_until_shutdown($server); } _execute($server, undef, 'sudo', 'virsh', 'start', $server->{name}); _determine_address($server); my $socket = _sleep_until_tcp_available($server); if ($socket) { close $socket; if ($server->{os} eq 'win32') { $server->{initial_command} = 'cd %TMP%'; _wait_for_server_to_boot($server); _cleanup_server($server); my $remote_tmp_directory = join q[], _remote_contents($server, undef, 'echo %TMP%'); $remote_tmp_directory =~ s/[\r\n]+$//smx; $remote_tmp_directory =~ s/\\/\//smxg; if (!$remote_tmp_directory) { die "Unable to find remote temp directory"; } my $cygwin_tmp_directory = $remote_tmp_directory; $cygwin_tmp_directory =~ s/^C:/\/cygdrive\/c/smx; my $local_username = getpwuid $EFFECTIVE_USER_ID; $server->{cygwin_command} = "cd $cygwin_tmp_directory"; _cleanup_cygwin($server); my $cover_db_format = join q[], _remote_contents($server, undef, 'echo %DEVEL_COVER_DB_FORMAT%'); $cover_db_format =~ s/[\r\n]+$//smx; $cover_db_format =~ s/\\/\//smxg; if ($cover_db_format ne $devel_cover_db_format) { die "Bad DEVEL_COVER_DB_FORMAT Environment variable"; } my $count = 0; REMOTE_WIN32_FIREFOX: { local $ENV{FIREFOX_NO_UPDATE} = 1; local $ENV{FIREFOX_USER} = $server->{user}; local $ENV{FIREFOX_HOST} = $server->{address}; $count += 1; my $start_execute_time = time; my $result = _execute($server, { alarm_after => $ENV{FIREFOX_ALARM}, return_result => 1 }, $^X, ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', $test_marionette_file); my $total_execute_time = time - $start_execute_time; if ($result != 0) { if ($count < $max_attempts) { my $error_message = _error_message($^X, $CHILD_ERROR); _log_stderr($server, "Failed '$^X$local_devel_cover_inc_with_space -Ilib $test_marionette_file' with FIREFOX_USER=$server->{user} and FIREFOX_HOST=$server->{address} at " . localtime . " exited with a '$error_message' after $total_execute_time seconds. Sleeping for $reset_time seconds"); if (_restart_server($server, $count)) { redo REMOTE_WIN32_FIREFOX; } else { die "Failed to restart remote $server->{name} on time $count"; } } else { die "Failed to make $count times"; } } } $count = 0; if ($server->{cygwin}) { REMOTE_CYGWIN_FIREFOX: { local $ENV{FIREFOX_NO_UPDATE} = 1; local $ENV{FIREFOX_USER} = $server->{user}; my $port = $server->{cygwin}; local $ENV{FIREFOX_PORT} = $port; local $ENV{FIREFOX_HOST} = $server->{address}; $count += 1; my $start_execute_time = time; my $result = _execute($server, { alarm_after => $ENV{FIREFOX_ALARM}, return_result => 1 }, $^X, ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', $test_marionette_file); my $total_execute_time = time - $start_execute_time; if ($result != 0) { if ($count < $max_attempts) { my $error_message = _error_message($^X, $CHILD_ERROR); _log_stderr($server, "Failed '$^X$local_devel_cover_inc_with_space -Ilib $test_marionette_file' with FIREFOX_USER=$server->{user} and FIREFOX_HOST=$server->{address}:$port at " . localtime . " exited with a '$error_message' after $total_execute_time seconds. Sleeping for $reset_time seconds"); if (_restart_server($server, $count)) { redo REMOTE_CYGWIN_FIREFOX; } else { die "Failed to restart remote $server->{name} on time $count"; } } else { die "Failed to make $count times"; } } } } _remote_execute($server, {}, "mkdir $remote_tmp_directory/firefox-marionette"); _execute($server, undef, 'scp', '-r', '-P', $server->{port}, Cwd::cwd() . '/lib', Cwd::cwd() . '/t', $server->{user} . q[@] . $server->{address} . q[:] . $remote_tmp_directory . '/firefox-marionette'); $server->{initial_command} .= "\\firefox-marionette"; my $local_ip_address = _get_best_local_ip_match($server->{address}); $server->{cygwin_command} = "cd $cygwin_tmp_directory/firefox-marionette"; foreach my $command ( $server->{cygwin} ? ( { cygwin => 1, alarm_after => $cygwin_local_alarm, command_line => "cd $cygwin_tmp_directory/firefox-marionette; RELEASE_TESTING=1 FIREFOX_NO_UPDATE=1 perl$devel_cover_inc_with_space -Ilib $test_marionette_file" }, { alarm_after => $cygwin_local_alarm, command_line => "C:\\\\cygwin64\\\\bin\\\\bash --login -c 'cd $cygwin_tmp_directory/firefox-marionette; RELEASE_TESTING=1 FIREFOX_NO_NETWORK=1 FIREFOX_NO_RECONNECT=1 WATERFOX=1 FIREFOX_NO_UPDATE=1 perl$devel_cover_inc_with_space -Ilib $test_marionette_file" }, { cygwin => 1, alarm_after => $cygwin_remote_alarm, command_line => "cd $cygwin_tmp_directory/firefox-marionette; FIREFOX_HOST=$local_ip_address FIREFOX_USER=$local_username RELEASE_TESTING=1 perl$devel_cover_inc_with_space -Ilib $test_marionette_file" }, { cygwin => 1, alarm_after => $cygwin_remote_alarm, command_line => "cd $cygwin_tmp_directory/firefox-marionette; FIREFOX_HOST=$local_ip_address FIREFOX_USER=firefox RELEASE_TESTING=1 perl$devel_cover_inc_with_space -Ilib $test_marionette_file" }, ) : (), { alarm_after => $win32_local_alarm, command_line => "set FIREFOX_NO_UPDATE=1 && set RELEASE_TESTING=1 && perl$devel_cover_inc_with_space -Ilib " . _win32_path($test_marionette_file) }, { alarm_after => $win32_remote_alarm, force_pseudo_terminal => 1, command_line => "set FIREFOX_NO_UPDATE=1 && set FIREFOX_NO_RECONNECT=1 && set RELEASE_TESTING=1 && set FIREFOX_HOST=$local_ip_address && set FIREFOX_USER=firefox && perl$devel_cover_inc_with_space -Ilib " . _win32_path($test_marionette_file) }, { alarm_after => $win32_local_alarm, command_line => "set FIREFOX_NO_UPDATE=1 && set FIREFOX_DEVELOPER=1 && set RELEASE_TESTING=1 && perl$devel_cover_inc_with_space -Ilib " . _win32_path($test_marionette_file) }, { alarm_after => $win32_local_alarm, command_line => "set FIREFOX_NO_UPDATE=1 && set FIREFOX_NIGHTLY=1 && set RELEASE_TESTING=1 && perl$devel_cover_inc_with_space -Ilib " . _win32_path($test_marionette_file) }, { alarm_after => $win32_local_alarm, command_line => "set FIREFOX_NO_UPDATE=1 && set WATERFOX=1 && set FIREFOX_NO_RECONNECT=1 set FIREFOX_NO_NETWORK=1 && set RELEASE_TESTING=1 perl$devel_cover_inc_with_space -Ilib " . _win32_path($test_marionette_file) }, { alarm_after => $win32_local_alarm, command_line => "set FIREFOX_NO_UPDATE=1 && set WATERFOX_VIA_FIREFOX=1 && set FIREFOX_NO_RECONNECT=1 && set FIREFOX_NO_NETWORK=1 && set RELEASE_TESTING=1 && perl$devel_cover_inc_with_space -Ilib " . _win32_path($test_marionette_file) }, ) { $count = 0; WIN32_FIREFOX: { $count += 1; my $start_execute_time = time; my $result = _remote_execute($server, { alarm_after => $command->{alarm_after}, return_result => 1, cygwin => $command->{cygwin}, force_pseudo_terminal => $command->{force_pseudo_terminal}, }, $command->{command_line}); my $total_execute_time = time - $start_execute_time; if ($result != 0) { if ($count < $max_attempts) { my $error_message = _error_message('ssh', $CHILD_ERROR); _log_stderr($server, "Failed '$command->{command_line}' at " . localtime . " exited with a '$error_message' after $total_execute_time seconds. Sleeping for $reset_time seconds"); _cleanup_win32($server); if (_restart_server($server, $count)) { redo WIN32_FIREFOX; } else { die "Failed to restart local $server->{name} on time $count"; } } else { die "Failed to make $count times"; } } } } if ($devel_cover_inc) { _execute($server, undef, 'scp', '-r', '-P', $server->{port}, $server->{user} . q[@] . $server->{address} . q[:/] . $remote_tmp_directory . q[/firefox-marionette/] . $cover_db_name, $cover_db_path . '/'); } _cleanup_server($server); _virsh_shutdown($server); _sleep_until_shutdown($server); } elsif ($server->{os} eq 'android') { my $count = 0; _execute($server, { alarm_after => $ENV{FIREFOX_ALARM}, return_result => 1 }, $^X, ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', '-MFirefox::Marionette', '-e', "Firefox::Marionette->new(adb => '$server->{address}');"); _execute($server, { alarm_after => $ENV{FIREFOX_ALARM}, return_result => 1 }, $^X, ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', '-MFirefox::Marionette', '-e', "Firefox::Marionette->new(adb => '$server->{address}', port => $server->{port});"); _execute($server, undef, 'adb', 'shell', 'poweroff'); } } else { die "SSH server is not detected"; } } elsif ((lc $server->{type}) eq 'physical') { my $socket = _sleep_until_tcp_available($server); if ($socket) { my $remote_tmp_directory = join q[], _remote_contents($server, undef, 'echo $TMPDIR'); _remote_execute($server, undef, "rm -Rf $remote_tmp_directory/firefox-marionette"); _remote_execute($server, undef, "mkdir $remote_tmp_directory/firefox-marionette"); _execute($server, undef, 'scp', '-r', '-P', $server->{port}, Cwd::cwd() . '/lib', Cwd::cwd() . '/t', $server->{user} . q[@] . $server->{address} . q[:] . $remote_tmp_directory . '/firefox-marionette'); $server->{initial_command} = "cd ${remote_tmp_directory}firefox-marionette"; my $count = 0; REMOTE_PHYSICAL_FIREFOX: { local $ENV{FIREFOX_NO_UPDATE} = 1; local $ENV{FIREFOX_NO_VISIBLE} = 1; local $ENV{FIREFOX_USER} = 'firefox'; local $ENV{FIREFOX_HOST} = $server->{address}; $count += 1; my $start_execute_time = time; my $result = _execute($server, { alarm_after => $ENV{FIREFOX_ALARM}, return_result => 1 }, $^X, ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', $test_marionette_file); my $total_execute_time = time - $start_execute_time; if ($result != 0) { if ($count < $max_attempts) { my $error_message = _error_message($^X, $CHILD_ERROR); _log_stderr($server, "Failed '$^X$local_devel_cover_inc_with_space -Ilib $test_marionette_file' with FIREFOX_USER=$server->{user} and FIREFOX_HOST=$server->{address} at " . localtime . " exited with a '$error_message' after $total_execute_time seconds. Sleeping for $reset_time seconds"); redo REMOTE_PHYSICAL_FIREFOX; } else { die "Failed to make $count times"; } } } my $via_address; my $remote_address; my $remote_user; foreach my $remote (@servers) { if ((lc $remote->{type}) eq 'virsh') { if ($remote->{os} eq 'win32') { while (!_virsh_node_running($remote)) { sleep 5; } _determine_address($remote); my $local_username = getpwuid $EFFECTIVE_USER_ID; my $jump_ip_address = _get_best_local_ip_match($server->{address}); $via_address = 'jump' . q[@] . $jump_ip_address; $remote_user = $remote->{user}; $remote_address = $remote->{address}; } } } foreach my $command ( { alarm_after => $win32_via_alarm, command_line => "DEVEL_COVER_DB_FORMAT=JSON RELEASE_TESTING=1 FIREFOX_VIA=$via_address FIREFOX_USER=$remote_user FIREFOX_HOST=$remote_address FIREFOX_NO_RECONNECT=1 FIREFOX_NO_UPDATE=1 perl$devel_cover_inc_with_space -Ilib $test_marionette_file" }, { alarm_after => $physical_local_alarm, command_line => "DEVEL_COVER_DB_FORMAT=JSON FIREFOX_NO_VISIBLE=1 RELEASE_TESTING=1 FIREFOX_NO_UPDATE=1 perl$devel_cover_inc_with_space -Ilib $test_marionette_file" }, { alarm_after => $physical_local_alarm, command_line => "DEVEL_COVER_DB_FORMAT=JSON FIREFOX_NO_VISIBLE=1 FIREFOX_DEVELOPER=1 RELEASE_TESTING=1 FIREFOX_NO_UPDATE=1 perl$devel_cover_inc_with_space -Ilib $test_marionette_file" }, { alarm_after => $physical_local_alarm, command_line => "DEVEL_COVER_DB_FORMAT=JSON FIREFOX_NO_VISIBLE=1 FIREFOX_NIGHTLY=1 RELEASE_TESTING=1 FIREFOX_NO_UPDATE=1 perl$devel_cover_inc_with_space -Ilib $test_marionette_file" }, { alarm_after => $physical_local_alarm, command_line => "DEVEL_COVER_DB_FORMAT=JSON FIREFOX_NO_VISIBLE=1 WATERFOX=1 FIREFOX_NO_RECONNECT=1 FIREFOX_NO_NETWORK=1 RELEASE_TESTING=1 FIREFOX_NO_UPDATE=1 perl$devel_cover_inc_with_space -Ilib $test_marionette_file" }, { alarm_after => $physical_local_alarm, command_line => "DEVEL_COVER_DB_FORMAT=JSON FIREFOX_NO_VISIBLE=1 WATERFOX_VIA_FIREFOX=1 FIREFOX_NO_RECONNECT=1 FIREFOX_NO_NETWORK=1 RELEASE_TESTING=1 FIREFOX_NO_UPDATE=1 perl$devel_cover_inc_with_space -Ilib $test_marionette_file" }, ) { $count = 0; REMOTE_FIREFOX: { $count += 1; my $start_execute_time = time; _remote_execute($server, undef, 'killall firefox || true'); _remote_execute($server, undef, 'killall perl || true'); my $result = _remote_execute($server, { alarm_after => $command->{alarm_after}, return_result => 1 }, $command->{command_line}); my $total_execute_time = time - $start_execute_time; if ($result != 0) { if ($count < $max_attempts) { my $error_message = _error_message('ssh', $CHILD_ERROR); _log_stderr($server, "Failed '$command->{command_line}' at " . localtime . " exited with a '$error_message' after $total_execute_time seconds. Sleeping for $reset_time seconds"); _remote_execute($server, undef, 'killall firefox || true'); _remote_execute($server, undef, 'killall perl || true'); redo REMOTE_FIREFOX; } else { die "Failed to make $count times"; } } } } if ($devel_cover_inc) { _execute($server, undef, 'scp', '-r', '-P', $server->{port}, $server->{user} . q[@] . $server->{address} . q[:] . $remote_tmp_directory . q[firefox-marionette/] . $cover_db_name, $cover_db_path . '/'); } } else { die "SSH server is not detected at $server->{address}"; } } else { die "Unknown server type '$server->{type}' in $servers_path"; } _log_stderr($server, "Test Suite for " . ($server->{name} || $server->{address}) . " has completed successfully"); exit 0; } or do { _log_stderr($server, "Caught an exception while remote testing:$EVAL_ERROR"); }; exit 1; } else { die "Failed to fork:$EXTENDED_OS_ERROR"; } } 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); my @entries; if ($handle) { 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; } } else { warn "No firefox den at $path"; } 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; } { _multiple_attempts_execute($^X, [ ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', $test_marionette_file ], {}); { local $ENV{FIREFOX_ALARM} = 2100; _multiple_attempts_execute($^X, [ ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', $test_marionette_file ], { FIREFOX_HOST => 'localhost', FIREFOX_FORCE_SCP => 1 }); } { local $ENV{FIREFOX_ALARM} = 2100; _multiple_attempts_execute($^X, [ ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', $test_marionette_file ], { FIREFOX_HOST => 'localhost:22' }); } { _multiple_attempts_execute('xvfb-run', [ '-a', $^X, ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', $test_marionette_file ]); } } _check_for_background_processes($background_pids, \@servers); if (my $entry = $old_versions{'firefox-upgrade'}) { setup_upgrade(); if (defined $paths_to_binary{$entry}) { { local $ENV{FIREFOX_ALARM} = 2700; _multiple_attempts_execute($^X, [ ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', $test_marionette_file ], { FIREFOX_HOST => 'localhost', FIREFOX_BINARY => $paths_to_binary{$entry} }); } } } _check_for_background_processes($background_pids, @servers); my $firefox_nightly_failed; ENTRY: foreach my $entry (reverse sort { $a cmp $b } @entries) { local %ENV = %ENV; my $old_version = $old_versions{$entry}; if (($entry =~ /firefox/smx) && ($old_version =~ /^[234]\d[.]/smx)) { $ENV{FIREFOX_NO_NETWORK} = 1; warn "Enabling FIREFOX_NO_NETWORK for Firefox version $old_version\n"; } else { delete $ENV{FIREFOX_NO_NETWORK}; } my $count = 0; my $path_to_binary = $paths_to_binary{$entry}; $ENV{FIREFOX_BINARY} = $path_to_binary; if ($entry =~ /^waterfox/smx) { warn "Disabling NETWORK and RECONNECT for Waterfox version $old_version\n"; _multiple_attempts_execute($^X, [ ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', $test_marionette_file ], { FIREFOX_NO_RECONNECT => 1, FIREFOX_NO_NETWORK => 1, WATERFOX => 1, FIREFOX_BINARY => $paths_to_binary{$entry} }); _multiple_attempts_execute($^X, [ ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', $test_marionette_file ], { FIREFOX_NO_RECONNECT => 1, FIREFOX_NO_NETWORK => 1, WATERFOX_VIA_FIREFOX => 1, FIREFOX_BINARY => $paths_to_binary{$entry} }); } else { $count = 0; LOCAL: { $count += 1; my $start_execute_time = time; my $result = system { $^X } $^X, ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', $test_marionette_file; my $total_execute_time = time - $start_execute_time; if ($result != 0) { if ($count < $max_attempts) { my $error_message = _error_message($^X, $CHILD_ERROR); warn "Failed '$^X$local_devel_cover_inc_with_space -Ilib $test_marionette_file with FIREFOX_BINARY=$ENV{FIREFOX_BINARY} at ' " . localtime . " exited with a '$error_message' after $total_execute_time seconds. 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() . '; FIREFOX_ALARM=' . $ENV{FIREFOX_ALARM} . ' DEVEL_COVER_DB_FORMAT=' . $devel_cover_db_format . ($ENV{FIREFOX_NO_NETWORK} ? ' FIREFOX_NO_NETWORK=1' : q[]) . ' RELEASE_TESTING=1 FIREFOX_BINARY="' . $ENV{FIREFOX_BINARY} . "\" $^X$local_devel_cover_inc_with_space -Ilib $test_marionette_file"; if ($entry eq 'firefox-nightly') { if (!_multiple_attempts_execute('ssh', [ 'localhost', $bash_command ], undef, 1)) { $firefox_nightly_failed = 1; next ENTRY; } } else { _multiple_attempts_execute('ssh', [ 'localhost', $bash_command ]); } if ($entry eq 'firefox-upgrade') { setup_upgrade(); } $bash_command = 'cd ' . Cwd::cwd() . '; FIREFOX_ALARM=' . $ENV{FIREFOX_ALARM} . ' DEVEL_COVER_DB_FORMAT=' . $devel_cover_db_format . ($ENV{FIREFOX_NO_NETWORK} ? ' FIREFOX_NO_NETWORK=1' : q[]) . ' RELEASE_TESTING=1 FIREFOX_VISIBLE=1 FIREFOX_BINARY="' . $ENV{FIREFOX_BINARY} . "\" $^X$local_devel_cover_inc_with_space -Ilib $test_marionette_file"; if ($entry eq 'firefox-nightly') { if (!_multiple_attempts_execute('ssh', [ 'localhost', $bash_command ], undef, 1)) { $firefox_nightly_failed = 1; next ENTRY; } } else { _multiple_attempts_execute('ssh', [ 'localhost', $bash_command ]); } } 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"; } _check_for_background_processes($background_pids, @servers); } foreach my $syscall_entry (@syscall_entries) { _multiple_attempts_execute($^X, [ ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', $syscall_entry ], {}); } _multiple_attempts_execute($^X, [ ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', '-wT', 't/04-proxy.t' ], {}); _multiple_attempts_execute($^X, [ ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', 't/04-webauthn.t' ], {}); _multiple_attempts_execute($^X, [ ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', 't/04-botd.t' ], {}); _multiple_attempts_execute($^X, [ ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', 't/04-browserfeatcl.t' ], {}); _multiple_attempts_execute($^X, [ ($local_devel_cover_inc ? $local_devel_cover_inc : ()), '-Ilib', 't/04-timezone.t' ], {}); while (_check_for_background_processes($background_pids, @servers)) { sleep 10; } foreach my $server (@servers) { if ((lc $server->{type}) eq 'virsh') { if (_virsh_node_running($server)) { _virsh_shutdown($server); _sleep_until_shutdown($server); } } } chdir $cwd or die "Failed to chdir to '$cwd':$EXTENDED_OS_ERROR"; if (-d "$cover_db_path/$cover_db_name") { $ENV{DEVEL_COVER_DB_FORMAT} = $devel_cover_db_format; system { 'cover' } 'cover', '-ignore_re', '^t/*', "$cover_db_path/$cover_db_name" and die "Failed to 'cover'"; system { 'mv' } 'mv', "$cover_db_path/$cover_db_name", "." and die "Failed to 'mv $cover_db_path/$cover_db_name .'"; } else { warn "No coverage generated\n"; } if ($background_failed) { warn "Background processes failed to complete successfully\n"; } if ($firefox_nightly_failed) { warn "Firefox Nightly failed to complete successfully\n"; } else { warn "Firefox Nightly PASSED successfully\n"; } my $start_time_for_journalctl = POSIX::strftime('%Y-%m-%d %H:%M:%S', localtime $start_time); my $ssh_auth_cmd = 'ssh-auth-cmd-marionette'; my $journalctl_handle = FileHandle->new(); if (my $pid = $journalctl_handle->open(q[-|])) { my $count = 0; while(my $line = <$journalctl_handle>) { $count += 1; } close $journalctl_handle or die "Failed to successfully execute journalctl:$EXTENDED_OS_ERROR"; warn "Total number of syslog messages generated by $ssh_auth_cmd is $count\n"; } elsif (defined $pid) { eval { exec { 'journalctl' } 'journalctl', '-t', 'ssh-auth-cmd-marionette', '--since', $start_time_for_journalctl, '--quiet' or die "Failed to exec 'journalctl':$EXTENDED_OS_ERROR"; } or do { warn "Failed to execute 'journalctl':$EVAL_ERROR"; }; exit 1; } else { die "Failed to fork:$EXTENDED_OS_ERROR"; } $journalctl_handle = FileHandle->new(); if (my $pid = $journalctl_handle->open(q[-|])) { my $count = 0; while(my $line = <$journalctl_handle>) { $count += 1; warn $line; } close $journalctl_handle or die "Failed to successfully execute journalctl:$EXTENDED_OS_ERROR"; warn "$ssh_auth_cmd generated $count warning messages\n"; } elsif (defined $pid) { eval { exec { 'journalctl' } 'journalctl', '-t', 'ssh-auth-cmd-marionette', '-p', 'warning', '--since', $start_time_for_journalctl, '--quiet' or die "Failed to exec 'journalctl':$EXTENDED_OS_ERROR"; } or do { warn "Failed to execute 'journalctl':$EVAL_ERROR"; }; exit 1; } else { die "Failed to fork:$EXTENDED_OS_ERROR"; } } sub _get_best_local_ip_match { # this is pretty dodgy, but good enough for these type of tests my ($remote_ip_addr) = @_; my $handle = FileHandle->new(); if (my $pid = $handle->open(q[-|])) { while(my $line = <$handle>) { if ($line =~ /^[ ]+inet[ ]((\d+[.]\d+[.]\d+)[.]\d+)/smx) { my ($local_ip_addr, $local_ip_prefix) = ($1, $2); my $quoted_local_ip_prefix = quotemeta $local_ip_prefix; if ($remote_ip_addr =~ /^$quoted_local_ip_prefix/smx) { return $local_ip_addr; } } } close $handle or die "Failed to successfully execute 'ip addr':$!"; } elsif (defined $pid) { eval { exec { 'ip' } 'ip', 'addr' or die "Failed to exec 'ip':$EXTENDED_OS_ERROR"; } or do { warn "Failed to execute 'ip addr':$EVAL_ERROR"; }; exit 1; } else { die "Failed to fork:$EXTENDED_OS_ERROR"; } return; } sub _wait_for_server_to_boot { my ($server) = @_; my $server_booted = 0; _log_stderr($server, "Waiting for SSH to start"); while(!$server_booted) { if (my $pid = fork) { waitpid $pid, 0; if ($CHILD_ERROR == 0) { $server_booted = 1; } else { _log_stderr($server, "SSH has not started yet"); } } elsif (defined $pid) { local $SIG{ALRM} = q[]; alarm 10; eval { exec { 'ssh' } 'ssh', $server->{user} . q[@] . $server->{address}, 'exit 0' or die "Failed to exec 'ssh':$EXTENDED_OS_ERROR"; } or do { warn "Failed to execute 'ip addr':$EVAL_ERROR"; }; exit 1; } else { die "Failed to fork:$EXTENDED_OS_ERROR"; } } _log_stderr($server, "SSH has started"); } sub _restart_server { my ($server, $count) = @_; _log_stderr($server, "Restarting $server->{name} at " . localtime); if (my $pid = fork) { my $start_time = time; waitpid $pid, 0; _log_stderr($server, "Restart sub process completed after " . (time - $start_time) . " seconds at " . localtime); if ($CHILD_ERROR != 0) { die "Restart process failed to complete successfully:" . _error_message('Restart process', $CHILD_ERROR); } _wait_for_server_to_boot($server); _log_stderr($server, "Restart successful at " . localtime); } elsif (defined $pid) { eval { _virsh_shutdown($server); _sleep_until_shutdown($server); _execute($server, undef, 'sudo', 'virsh', 'start', $server->{name}); _determine_address($server); my $socket = _sleep_until_tcp_available($server); if ($socket) { close $socket; exit 0; } 0; } or do { _log_stderr($server, "Caught an exception while restarting $server->{address}:$EVAL_ERROR"); }; exit 1; } else { die "Failed to fork:$EXTENDED_OS_ERROR"; } } sub _check_for_background_processes { my ($background_pids, $servers) = @_; foreach my $pid (sort { $a <=> $b } keys %{$background_pids}) { my $result = waitpid $pid, POSIX::WNOHANG(); if ($result == $pid) { delete $background_pids->{$pid}; if ($CHILD_ERROR) { $background_failed = 1; } } elsif ($result == -1) { warn "Background process $pid has already been reaped at " . localtime . "\n"; delete $background_pids->{$pid}; } else { return 1; } } if (%{$background_pids}) { foreach my $server (@{$servers}) { if ((lc $server->{type}) eq 'virsh') { _virsh_shutdown($server); _sleep_until_shutdown($server); } } } return 0; } sub _test_description { my ($command, $arguments, $env) = @_; my $description = q['] . (join q[ ], $command, @{$arguments}) . q[']; if ((ref $env) && (keys %{$env})) { $description .= q[ with ] . join q[ and ], map { "$_=$env->{$_}" } sort { $a cmp $b } keys %{$env}; } return $description; } sub _multiple_attempts_execute { my ($command, $arguments, $env, $skip_on_fail) = @_; local %ENV = %ENV; my $count = 0; ATTEMPT: { foreach my $key (sort { $a cmp $b } keys %{$env}) { $ENV{$key} = $env->{$key}; } $count += 1; my $start_execute_time = time; my $result = system { $command } $command, @{$arguments}; my $total_execute_time = time - $start_execute_time; if ($result != 0) { if ($count < $max_attempts) { my $error_message = _error_message($command, $CHILD_ERROR); warn q[Failed ] . _test_description($command, $arguments, $env) . q[ at ] . localtime . " exited with a '$error_message' after $total_execute_time seconds. Sleeping for $reset_time seconds"; sleep $reset_time; redo ATTEMPT; } else { die q[Failed to ] . _test_description($command, $arguments, $env) . " $count times"; } } } return 1; } sub _win32_path { my ($unix_path) = @_; my $windows_path = join q[\\], split /[\/]/smx, $unix_path; return $windows_path; } sub _check_parent_alive { if (!kill 0, $parent_pid) { die "Parent ($parent_pid) is no longer running. Terminating\n"; } } sub _sleep_until_shutdown { my ($server) = @_; while (_virsh_node_running($server)) { _virsh_shutdown($server); _log_stderr($server, "Waiting for $server->{name} to shutdown"); sleep 1; } return; } sub _determine_address { my ($server) = @_; if (!$server->{address}) { my $address; while(!($address = _get_address($server))) { if (_virsh_node_running($server)) { _log_stderr($server, "Waiting for $server->{name} to get an IP address"); sleep 1; } else { return; } } $server->{address} = $address; } } sub _sleep_until_tcp_available { my ($server) = @_; my $client_socket; while(!($client_socket = IO::Socket->new( Domain => IO::Socket::AF_INET(), Type => IO::Socket::SOCK_STREAM(), proto => 'tcp', PeerPort => $server->{port}, PeerHost => $server->{address}, ))) { if (_virsh_node_running($server)) { _log_stderr($server, "Waiting for $server->{name} to start a TCP server on port $server->{port}"); sleep 1; } else { _log_stderr($server, "Server $server->{name} has stopped running while waiting for TCP server to start on port $server->{port}"); return; } } _log_stderr($server, ($server->{name} || $server->{address}) . " has started the TCP server on port $server->{port}"); return $client_socket; } sub _virsh_node_running { my ($server) = @_; my $running = 0; foreach my $line (_contents($server, undef, 'sudo', 'virsh','list', '--name')) { if ($line =~ /^\s*$server->{name}\s*$/smx) { $running = 1; } } return $running; } sub _cleanup_cygwin { my ($server) = @_; my $parameters; foreach my $line (_list_remote_cygwin_tmp_directory($server, { cygwin => 1 })) { if ($line =~ /^(firefox\-marionette)\s*$/smx) { _rmdir($server, $1); } elsif ($line =~ /^(firefox_marionette_selfie_\S+)\s*$/smx) { _unlink($server, $1); } elsif ($line =~ /^(firefox_marionette_\S+)\s*$/smx) { _rmdir($server, $1); } elsif ($line =~ /^(firefox_test_part_cert_\S+)\s*$/smx) { _unlink($server, $1); } elsif ($line =~ /^(firefox_test_part_cert_\S+)\s*$/smx) { _unlink($server, $1); } elsif ($line =~ /^(tmpaddon\S*)\s*$/smx) { _unlink($server, $1); } elsif ($line =~ /^(mozilla\-temp\-files)\s*$/smx) { _rmdir($server, $1); } elsif ($line =~ /^(MozillaBackgroundTask\S+backgroundupdate\S*)\s*$/smx) { _rmdir($server, $1); } } } sub _list_remote_cygwin_tmp_directory { my ($server, $parameters) = @_; return _remote_contents($server, $parameters, 'ls -1'); } sub _cleanup_server { my ($server) = @_; my $parameters; foreach my $line (_list_remote_tmp_directory($server)) { if ($line =~ /^(firefox\-marionette)\s*$/smx) { _rmdir($server, $1); } elsif ($line =~ /^(firefox_marionette_selfie_\S+)\s*$/smx) { _unlink($server, $1); } elsif ($line =~ /^(firefox_marionette_\S+)\s*$/smx) { _rmdir($server, $1); } elsif ($line =~ /^(perl_ff_m_\S+)\s*$/smx) { _rmdir($server, $1); } elsif ($line =~ /^(firefox_test_part_cert_\S+)\s*$/smx) { _unlink($server, $1); } elsif ($line =~ /^(firefox_test_part_cert_\S+)\s*$/smx) { _unlink($server, $1); } elsif ($line =~ /^(tmpaddon\S*)\s*$/smx) { _unlink($server, $1); } elsif ($line =~ /^(mozilla\-temp\-files)\s*$/smx) { _rmdir($server, $1); } elsif ($line =~ /^(MozillaBackgroundTask\S+backgroundupdate\S*)\s*$/smx) { _rmdir($server, $1); } } } sub _virsh_shutdown { my ($server) = @_; if ($server->{os} eq 'android') { _execute($server, undef, 'adb', 'connect', (join q[:], $server->{address}, $server->{port})); _execute($server, undef, 'adb', '-s', (join q[:], $server->{address}, $server->{port}), 'shell', 'poweroff'); _execute($server, undef, 'adb', 'kill-server'); my $adb_log_file = File::Spec->catfile(File::Spec->tmpdir(), 'adb.' . $> . '.log'); unlink $adb_log_file or ($! == POSIX::ENOENT()) or die "Failed to unlink $adb_log_file:$!"; } else { return _execute($server, undef, 'sudo', 'virsh', 'shutdown', $server->{name}); } } sub _unlink { my ($server, $filename) = @_; _remote_execute($server, undef, 'del /f /q ' . $filename); } sub _rmdir { my ($server, $directory) = @_; _remote_execute($server, undef, 'rmdir /s /q ' . $directory); } sub _execute { my ($server, $parameters, $command, @arguments) = @_; return _contents($server, $parameters, $command, @arguments); } sub _remote_execute { my ($server, $parameters, $remote_command_line) = @_; return _remote_contents($server, $parameters, $remote_command_line); } sub _remote_contents { my ($server, $parameters, $remote_command_line) = @_; my $initial_command; if ($parameters->{cygwin}) { $initial_command = $server->{cygwin_command}; } else { $initial_command = $server->{initial_command}; } return _contents($server, $parameters, 'ssh', _ssh_parameters($parameters), _server_address($server, $parameters), join q[ && ], grep { defined } $initial_command, $remote_command_line); } sub _ssh_parameters { my ($parameters) = @_; return ( '-2', ($parameters->{force_pseudo_terminal} ? ('-t', '-t') : ()), '-o', 'BatchMode=yes', '-o', 'ServerAliveCountMax=5', '-o', 'ServerAliveInterval=3', ); } sub _server_address { my ($server, $parameters) = @_; return ('-p', ($parameters->{cygwin} ? $server->{cygwin} : $server->{port}), $server->{user} . q[@] . $server->{address}); } sub _list_remote_tmp_directory { my ($server) = @_; return _remote_contents($server, undef, 'dir /B'); } sub _get_address { my ($server) = @_; my $address; foreach my $line (_contents($server, undef, 'sudo', 'virsh', 'domifaddr', $server->{name})) { if ($line =~ /^\s+\w+\s+[a-f0-9:]+\s+ipv4\s+([\d.]+)\/24\s*$/smx) { ($address) = ($1); } } return $address; } sub _prefix { my ($server) = @_; return ($server->{name} || $server->{address}). ' --> '; } sub _log_stderr { my ($server, $message) = @_; print {*STDERR} _prefix($server) . "$message\n" or die "Failed to print to STDERR:$EXTENDED_OS_ERROR"; } sub _log_stdout { my ($server, $message) = @_; print _prefix($server) . "$message\n" or die "Failed to print to STDOUT:$EXTENDED_OS_ERROR"; } sub _contents { my ($server, $parameters, $command, @arguments) = @_; _check_parent_alive(); my @lines; my $return_result; my $handle = FileHandle->new(); if (my $pid = $handle->open(q[-|])) { my $alarm_method; my $alarm_killed; if ($parameters->{alarm_after}) { _log_stderr($server, "Alarm is $parameters->{alarm_after} seconds"); alarm $parameters->{alarm_after}; $alarm_method = sub { _log_stderr($server, "Killing local process $pid after $parameters->{alarm_after} seconds at " . localtime); while (kill 0, $pid) { kill 'TERM', $pid; sleep 1; waitpid $pid, POSIX::WNOHANG(); } _log_stderr($server, "Killed local process $pid after $parameters->{alarm_after} seconds at " . localtime); $alarm_killed = 1; }; } local $SIG{ALRM} = $alarm_method; COMMAND: while(my $line = <$handle>) { $line =~ s/\r?\n$//smx; $line =~ s/\e\[(K|\d+;1H|\??25[lh]|2J|[mHG]|23X|17X)//smxg; $line =~ s/\e\]0;//smxg; $line =~ s/\x7//smxg; _check_parent_alive(); _log_stdout($server, $line); push @lines, $line; if ($alarm_killed) { last COMMAND; } } if (!$alarm_killed) { my $result = close $handle; if ($result == 1) { $return_result = 0; } else { if ($ERRNO == 0) { _log_stderr($server, "Command " . (join q[ ], $command, @arguments) . " failed to close successfully:" . _error_message($command, $CHILD_ERROR)); } else { _log_stderr($server, "Command " . (join q[ ], $command, @arguments) . " failed to cleanup successfully:$!:"); } $return_result = 1; } } else { _log_stderr($server, "Command " . (join q[ ], $command, @arguments) . " killed by TERM after alarm time of $parameters->{alarm_after} was exceeded:" . _error_message($command, $CHILD_ERROR)); $return_result = 1; } alarm 0; } elsif (defined $pid) { eval { open STDERR, '<&=', fileno STDOUT or die "Failed to redirect STDERR:$EXTENDED_OS_ERROR"; exec { $command } $command, @arguments or die "Failed to exec $command:$EXTENDED_OS_ERROR"; } or do { _log_stderr($server, q[Caught an exception while running '] . (join q[ ], $command, @arguments) . "':$EVAL_ERROR"); }; exit 1; } else { die "Failed to fork:$EXTENDED_OS_ERROR"; } if ($parameters->{return_result}) { return $return_result; } else { return @lines; } } sub _signal_name { my ( $number ) = @_; return $sig_names[$number]; } sub _error_message { my ($binary, $child_error) = @_; my $message; 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 = _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 _cleanup_win32 { my ($server) = @_; foreach my $exe_name (qw(firefox.exe perl.exe)) { my $command = qq[taskkill /T /F /IM $exe_name || exit 0]; _log_stderr($server, "Cleaning up Win32 with '$command' at " . localtime); _remote_execute($server, undef, $command); } } END { my $end_time = time; my ($hours, $minutes, $seconds) = (0,0,$end_time - $start_time); while($seconds >= 3600) { $seconds -= 3600; $hours += 1; } while($seconds >= 60) { $seconds -= 60; $minutes += 1; } print "Run took $hours hours, $minutes minutes and $seconds seconds\n"; print "End time is " . (localtime $end_time) . "\n"; } Firefox-Marionette-1.63/t/03-sysopen.t0000644000175000017500000000077214547652665016174 0ustar davedave#! /usr/bin/perl -w use strict; use lib qw(t/); use syscall_tests (qw(sysopen)); *CORE::GLOBAL::sysopen = sub { my $handle = CORE::sysopen $_[0], $_[1], $_[2]; if (($handle) && (syscall_tests::allow())) { return $handle } else { $! = POSIX::EACCES(); return } }; require Firefox::Marionette; syscall_tests::run(POSIX::EACCES()); syscall_tests::visible(POSIX::EACCES()); no warnings; *CORE::GLOBAL::sysopen = sub { return CORE::sysopen $_[0], $_[1], $_[2]; }; use warnings; syscall_tests::finalise(); Firefox-Marionette-1.63/t/03-mkdir.t0000644000175000017500000000065114547652665015576 0ustar davedave#! /usr/bin/perl -w use strict; use lib qw(t/); use syscall_tests (qw(mkdir)); *CORE::GLOBAL::mkdir = sub { if (syscall_tests::allow()) { CORE::mkdir $_[0]; } else { $! = POSIX::EACCES(); return } }; require Firefox::Marionette; syscall_tests::run(POSIX::EACCES()); syscall_tests::visible(POSIX::EACCES()); no warnings; *CORE::GLOBAL::mkdir = sub { return CORE::mkdir $_[0]; }; use warnings; syscall_tests::finalise(); Firefox-Marionette-1.63/t/04-webauthn.t0000644000175000017500000002726414640242476016304 0ustar davedave#! /usr/bin/perl -w use strict; use Firefox::Marionette(); use Crypt::URandom(); use MIME::Base64(); use Test::More; $SIG{INT} = sub { die "Caught an INT signal"; }; $SIG{TERM} = sub { die "Caught a TERM signal"; }; SKIP: { if (!$ENV{RELEASE_TESTING}) { plan skip_all => "Author tests not required for installation"; } if ($^O eq 'MSWin32') { plan skip_all => "Cannot test in a $^O environment"; } my $profile = Firefox::Marionette::Profile->new(); my @extra_parameters; if ($ENV{FIREFOX_BINARY}) { push @extra_parameters, (binary => $ENV{FIREFOX_BINARY}); } my $debug = $ENV{FIREFOX_DEBUG} || 0; my $visible = $ENV{FIREFOX_VISIBLE} || 0; my $loop_max = $ENV{FIREFOX_MAX_LOOP} || 1; my $firefox = Firefox::Marionette->new( @extra_parameters, debug => $debug, visible => $visible, profile => $profile, devtools => $debug && $visible, ); ok($firefox, "Created a firefox object"); my $user_name = MIME::Base64::encode_base64( Crypt::URandom::urandom( 10 ), q[] ) . q[@example.com]; ok($user_name, "User is $user_name"); my $host_name = 'webauthn.io'; my ($major_version, $minor_version, $patch_version) = split /[.]/,$firefox->browser_version(); if ($major_version >= 118) { my $result; eval { $result = $firefox->go('https://' . $host_name); }; ok($result, "Loading https://$host_name:$@"); ok($firefox->find_id('input-email')->type($user_name), "Entering $user_name for username"); ok($firefox->find_id('register-button')->click(), "Registering authentication for $host_name");; $firefox->await(sub { sleep 1; $firefox->find_class('alert-success'); }); ok($firefox->find_id('login-button')->click(), "Clicking login button for $host_name"); ok($firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); }), "Successfully authenticated to $host_name"); my $authenticator = $firefox->webauthn_authenticator(); ok($authenticator, "Successfully retrieved WebAuthn Authenticator:" . $authenticator->id()); my $sign_count_after_one_login; my $count = 0; foreach my $credential ($firefox->webauthn_credentials()) { ok($credential->id(), "Credential id is " . $credential->id()); ok($credential->host() eq $host_name, "Hostname is $host_name:" . $credential->host()); ok($credential->user(), "Username is " . $credential->user()); $sign_count_after_one_login = $credential->sign_count(); ok($credential->sign_count() >= 1, "Sign Count is >= 1:" . $credential->sign_count()); $firefox->delete_webauthn_credential($credential); $firefox->add_webauthn_credential( id => $credential->id(), host => $credential->host(), user => $credential->user(), private_key => $credential->private_key(), is_resident => $credential->is_resident(), sign_count => $credential->sign_count(), ); } ok($firefox->go('about:blank'), "Loading about:blank"); ok($firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_COOKIES()), "Deleting all cookies"); ok($firefox->go('https://' . $host_name), "Loading https://$host_name"); ok($firefox->find_id('input-email')->type($user_name), "Entering $user_name for username"); ok($firefox->find_id('login-button')->click(), "Clicking login button for $host_name"); ok($firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); }), "Successfully authenticated to $host_name"); $count = 0; foreach my $credential ($firefox->webauthn_credentials($authenticator)) { ok($credential->id(), "Credential id is " . $credential->id()); ok($credential->host() eq $host_name, "Hostname is $host_name:" . $credential->host()); ok($credential->user(), "Username is " . $credential->user()); ok($credential->sign_count() == ($sign_count_after_one_login * 2), "Sign Count is == ($sign_count_after_one_login * 2):" . $credential->sign_count()); } ok($firefox->delete_webauthn_all_credentials($authenticator), "Deleted all Webauthn credentials"); $host_name = 'webauthn.bin.coffee'; ok($firefox->go("https://$host_name"), "Loaded https://$host_name"); ok($firefox->find_id('createButton')->click(), "Clicked 'Create Credential'"); ok($firefox->find_id('getButton')->click(), "Clicked 'Get Assertion'"); foreach my $credential ($firefox->webauthn_credentials($authenticator)) { ok($credential->id(), "Credential id is " . $credential->id()); ok($credential->host() eq $host_name, "Hostname is " . $credential->host()); ok($credential->user(), "Username is " . $credential->user()); ok($credential->sign_count(), "Sign Count is " . $credential->sign_count()); } $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::HYBRID(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2(), has_resident_key => 1 ); ok($authenticator, "Successfully added CTAP2/Hybrid WebAuthn Authenticator:" . $authenticator->id()); ok($authenticator->transport() eq 'hybrid', "Correct transport of 'hybrid' is returned:" . $authenticator->transport()); ok($authenticator->protocol() eq 'ctap2', "Correct protocol of 'ctap2' is returned:" . $authenticator->protocol()); ok($authenticator->has_resident_key() == 1, "Correct value for has_resident_key is returned:" . $authenticator->has_resident_key()); $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2_1(), has_resident_key => 0, is_user_verified => 1 ); ok($authenticator, "Successfully added CTAP2_1/Internal WebAuthn Authenticator:" . $authenticator->id()); ok($authenticator->transport() eq 'internal', "Correct transport of 'internal' is returned:" . $authenticator->transport()); ok($authenticator->protocol() eq 'ctap2_1', "Correct protocol of 'ctap2_1' is returned:" . $authenticator->protocol()); ok($authenticator->has_resident_key() == 0, "Correct value for has_resident_key is returned:" . $authenticator->has_resident_key()); ok($authenticator->is_user_verified() == 1, "is_user_verified() is 1:" . $authenticator->is_user_verified()); $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::NFC(), has_user_verification => 0, is_user_consenting => 1 ); ok($authenticator, "Successfully added NFC WebAuthn Authenticator:" . $authenticator->id()); ok($authenticator->transport() eq 'nfc', "Correct transport of 'nfc' is returned:" . $authenticator->transport()); ok($authenticator->has_user_verification() == 0, "has_user_verification == 0:" . $authenticator->has_user_verification()); ok($authenticator->is_user_consenting() == 1, "is_user_consenting == 1:" . $authenticator->is_user_consenting()); $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::SMART_CARD(), has_user_verification => 1, is_user_consenting => 0 ); ok($authenticator, "Successfully added Smart Card WebAuthn Authenticator:" . $authenticator->id()); ok($authenticator->transport() eq 'smart-card', "Correct transport of 'smart-card' is returned:" . $authenticator->transport()); ok($authenticator->has_user_verification() == 1, "has_user_verification == 1:" . $authenticator->has_user_verification()); ok($authenticator->is_user_consenting() == 0, "is_user_consenting == 0:" . $authenticator->is_user_consenting()); $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::USB(), is_user_verified => 0 ); ok($authenticator, "Successfully added USB WebAuthn Authenticator:" . $authenticator->id()); ok($authenticator->transport() eq 'usb', "Correct transport of 'usb' is returned:" . $authenticator->transport()); ok($authenticator->is_user_verified() == 0, "is_user_verified() is 0:" . $authenticator->is_user_verified()); ok($firefox->webauthn_set_user_verified(undef, $authenticator), "verify webauthn user to default (true)"); ok($firefox->webauthn_set_user_verified(0), "verify webauthn user to false"); ok($firefox->webauthn_set_user_verified(1), "verify webauthn user to true"); my $credential = Firefox::Marionette::WebAuthn::Credential->new( host => 'example.org', is_resident => 1 ); ok($credential, "Webauthn credential created"); ok(!$credential->id(), "Credential id does not exist yet"); ok($credential->host(), "Hostname is " . $credential->host()); ok($credential->is_resident() == 1, "is_resident is 1:" . $credential->is_resident()); my $cred_id = 'rDFWHBYyRzQGhu92NBf6P6QOhGlsvjtxZB8b8GBWhwg'; $host_name = 'example.net'; eval { $credential = $firefox->add_webauthn_credential( authenticator => $authenticator, id => $cred_id, host => $host_name, is_resident => 0); }; foreach my $credential ($firefox->webauthn_credentials($authenticator)) { ok($credential->id() eq $cred_id, "Credential id is '$cred_id':" . $credential->id()); ok($credential->host() eq $host_name, "Hostname is '$host_name':" . $credential->host()); ok(!$credential->user(), "Username is empty"); ok($credential->is_resident() == 0, "is_resident is 0:" . $credential->is_resident()); ok($credential->sign_count() == 0, "Sign Count is 0:" . $credential->sign_count()); $firefox->delete_webauthn_credential($credential, $authenticator); } eval { $credential = $firefox->add_webauthn_credential( private_key => { name => 'RSA-PSS', size => 1024 }, host => $host_name, user => $user_name); ok($credential->id(), "Credential id is " . $credential->id()); ok($credential->host() eq $host_name, "Hostname is '$host_name':" . $credential->host()); ok($credential->user() eq $user_name, "Username is '$user_name':" . $credential->user()); }; ok($firefox->delete_webauthn_authenticator($authenticator), "Deleted virtual authenticator"); my $default_authenticator = $firefox->webauthn_authenticator(); ok($default_authenticator, "Default virtual authenticator still exists"); ok($firefox->delete_webauthn_authenticator(), "Deleted default virtual authenticator"); ok(!$firefox->webauthn_authenticator(), "Default virtual authenticator no longer exists"); $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::BLE(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP1_U2F() ); ok($authenticator, "Successfully added CTAP1_U2F/BLE WebAuthn Authenticator:" . $authenticator->id()); ok($authenticator->transport() eq 'ble', "Correct transport of 'ble' is returned:" . $authenticator->transport()); ok($authenticator->protocol() eq 'ctap1/u2f', "Correct protocol of 'ctap1/u2f' is returned:" . $authenticator->protocol()); ok($firefox->delete_webauthn_authenticator($authenticator), "Deleted virtual authenticator"); eval { $firefox->delete_webauthn_authenticator($default_authenticator); }; chomp $@; ok($@, "Failed to delete non-existant authenticator:$@"); $firefox = Firefox::Marionette->new( @extra_parameters, debug => $debug, visible => $visible, profile => $profile, devtools => $debug && $visible, webauthn => 0, ); ok(!$firefox->webauthn_authenticator(), "Default virtual authenticator was not created"); $firefox = Firefox::Marionette->new( @extra_parameters, debug => $debug, visible => $visible, profile => $profile, devtools => $debug && $visible, webauthn => 1, ); ok($firefox->webauthn_authenticator(), "Default virtual authenticator was created"); } else { diag("Webauthn not available for versions less than 118"); eval { $firefox = Firefox::Marionette->new( @extra_parameters, debug => $debug, visible => $visible, profile => $profile, devtools => $debug && $visible, webauthn => 1, ); 1; }; chomp $@; ok(1, "What happens when we force a webauthn authenticator to be added when it's not supported:$@"); } } done_testing(); Firefox-Marionette-1.63/t/pod.t0000755000175000017500000000021414547652665015030 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.63/t/03-closedir.t0000644000175000017500000000073414547652665016276 0ustar davedave#! /usr/bin/perl -w use strict; use Archive::Zip(); use XML::Parser(); use lib qw(t/); use syscall_tests (qw(closedir)); *CORE::GLOBAL::closedir = sub { if (syscall_tests::allow()) { CORE::closedir $_[0]; } else { $! = POSIX::EBADF(); return } }; require Firefox::Marionette; syscall_tests::run(POSIX::EBADF()); syscall_tests::visible(POSIX::EBADF()); no warnings; *CORE::GLOBAL::closedir = sub { return CORE::closedir $_[0]; }; use warnings; syscall_tests::finalise(); Firefox-Marionette-1.63/t/00.load.t0000755000175000017500000000021514547652665015404 0ustar davedaveuse Test::More tests => 1; BEGIN { use_ok( 'Firefox::Marionette' ); } diag( "Testing Firefox::Marionette $Firefox::Marionette::VERSION" ); Firefox-Marionette-1.63/t/04-fingerprint.t0000644000175000017500000000634014571052132016775 0ustar davedave#! /usr/bin/perl -w use strict; use Firefox::Marionette(); use Test::More; use File::Spec(); use lib qw(t/); $SIG{INT} = sub { die "Caught an INT signal"; }; $SIG{TERM} = sub { die "Caught a TERM signal"; }; SKIP: { if (!$ENV{RELEASE_TESTING}) { plan skip_all => "Author tests not required for installation"; } if ($^O eq 'MSWin32') { plan skip_all => "Cannot test in a $^O environment"; } require test_daemons; if (!Test::Daemon::FingerprintJS->fingerprintjs_available()) { plan skip_all => "FingerprintJS does not appear to be available"; } if (!Test::Daemon::FingerprintJS->available()) { plan skip_all => "yarn does not appear to be available in $ENV{PATH}"; } my $debug = $ENV{FIREFOX_DEBUG} || 0; my $visible = $ENV{FIREFOX_VISIBLE} || 0; my $fingerprintjs_listen = '127.0.0.1'; my $fingerprintjs = Test::Daemon::FingerprintJS->new(listen => $fingerprintjs_listen); ok($fingerprintjs, "Started FingerprintJS Server on $fingerprintjs_listen on port " . $fingerprintjs->port() . ", with pid " . $fingerprintjs->pid()); $fingerprintjs->wait_until_port_open(); my $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, ); ok($firefox, "Created a firefox object in normal mode"); my ($major_version, $minor_version, $patch_version) = split /[.]/, $firefox->capabilities()->browser_version(); TODO: { local $TODO = $major_version <= 122 ? 'Older firefoxen may be trackable' : q[]; ok(!_am_i_trackable_by_fingerprintjs($firefox, $fingerprintjs), "FingerprintJS cannot track this browser"); } ok($firefox->quit() == 0, "Firefox closed successfully"); $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, trackable => 1, ); ok($firefox, "Created a firefox object in trackable mode"); ok(_am_i_trackable_by_fingerprintjs($firefox, $fingerprintjs), "FingerprintJS CAN track this browser"); ok($firefox->quit() == 0, "Firefox closed successfully"); $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, trackable => 0, ); ok($firefox, "Created a firefox object in reset trackable mode"); TODO: { local $TODO = $major_version <= 122 ? 'Older firefoxen may be trackable' : q[]; ok(!_am_i_trackable_by_fingerprintjs($firefox, $fingerprintjs), "FingerprintJS cannot track this browser"); } ok($firefox->quit() == 0, "Firefox closed successfully"); } sub _am_i_trackable_by_fingerprintjs { my ($firefox, $fingerprintjs) = @_; ok($firefox->go(q[http://] . $fingerprintjs->address() . q[:] . $fingerprintjs->port()), q[Retrieved fingerprintjs page from http://] . $fingerprintjs->address() . q[:] . $fingerprintjs->port()); my $original_fingerprint = $firefox->await(sub { $firefox->find_class('giant'); })->text(); ok($original_fingerprint, "Found fingerprint of $original_fingerprint"); ok($firefox->restart(), "Restart firefox"); ok($firefox->go(q[http://] . $fingerprintjs->address() . q[:] . $fingerprintjs->port()), q[Retrieved fingerprintjs page from http://] . $fingerprintjs->address() . q[:] . $fingerprintjs->port()); my $new_fingerprint = $firefox->await(sub { $firefox->find_class('giant'); })->text(); ok($new_fingerprint, "Found fingerprint of $new_fingerprint"); return $new_fingerprint eq $original_fingerprint; } done_testing(); Firefox-Marionette-1.63/t/syscall_tests.pm0000644000175000017500000000742314615647672017317 0ustar davedavepackage syscall_tests; use strict; use warnings; use Test::More; use File::Spec(); use Fcntl(); use File::Path(); use Cwd(); BEGIN { if ($^O eq 'MSWin32') { require Win32; require Win32::Process; require Win32API::Registry; } } my $base_directory; my $syscall_count = 0; my $syscall_error_at_count = 0; my $function; my %parameters; my @CHARS = (qw/ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9 _ /); sub import { my $class = shift @_; $function = shift; unless ($ENV{RELEASE_TESTING}) { plan( skip_all => "Author tests not required for installation" ); } if ($^O eq 'MSWin32') { plan( skip_all => "Syscall tests not reliable for $^O"); } $base_directory = File::Spec->catdir(File::Spec->tmpdir(), 'firefox_marionette_test_suite_syscall_' . 'X' x 11); my $end = ( $] >= 5.006 ? "\\z" : "\\Z" ); $base_directory =~ s/X(?=X*$end)/$CHARS[ int( rand( @CHARS ) ) ]/gesmx; mkdir $base_directory, Fcntl::S_IRWXU() or die "Failed to create temporary directory:$!"; $ENV{TMPDIR} = $base_directory; return; } sub allow { my ($package, $file, $line) = caller; if ((defined $syscall_error_at_count) && ($syscall_count == $syscall_error_at_count)) { $syscall_count += 1; return 0; } else { $syscall_count += 1; return 1; } } sub run { my ($class, $expected_error_as_posix) = @_; my $cwd = Cwd::cwd(); %parameters = ( binary => File::Spec->catfile($cwd, 't', 'stub.pl'), har => 1, stealth => 1, ); my $success = 0; while(!$success) { $syscall_count = 0; eval { my $firefox = Firefox::Marionette->new(%parameters); $firefox->pdf(); $firefox->selfie(); $firefox->import_bookmarks(File::Spec->catfile(Cwd::cwd(), qw(t data bookmarks_empty.html))); $firefox->agent(version => 100); my $final = $syscall_error_at_count; $syscall_error_at_count = undef; ok($syscall_count >= 0 && $firefox->quit() == 0, "Firefox exited okay after $final successful $function calls"); $success = 1; } or do { chomp $@; my $actual_error_message = $@; my $expected_error_message = quotemeta POSIX::strerror($expected_error_as_posix); ok($actual_error_message =~ /(?:$expected_error_message|[ ]exited[ ]with[ ]a[ ][1])/smx, "Firefox failed with $function count set to $syscall_error_at_count:" . $actual_error_message); $syscall_error_at_count += 1; }; } my $firefox = Firefox::Marionette->new(%parameters); ok($firefox->quit() == 0, "Firefox exited okay when $function is reset"); } sub visible { my ($class, $expected_error_as_posix) = @_; $syscall_error_at_count = 0; delete $ENV{DISPLAY}; $parameters{visible} = 1; my $success = 0; while(!$success) { $syscall_count = 0; eval { my $firefox = Firefox::Marionette->new(%parameters); $firefox->pdf(); $firefox->selfie(); $firefox->import_bookmarks(File::Spec->catfile(Cwd::cwd(), qw(t data bookmarks_empty.html))); my $final = $syscall_error_at_count; $syscall_error_at_count = undef; ok($syscall_count > 0 && $firefox->quit() == 0, "Firefox (visible => 1) exited okay after $final successful $function calls"); $success = 1; } or do { chomp $@; my $actual_error_message = $@; my $expected_error_message = quotemeta POSIX::strerror($expected_error_as_posix); ok($actual_error_message =~ /(?:$expected_error_message|[ ]exited[ ]with[ ]a[ ][1])/smx, "Firefox (visible => 1) failed with $function count set to $syscall_error_at_count:" . $actual_error_message); $syscall_error_at_count += 1; }; } } sub finalise { my ($class) = @_; my $firefox = Firefox::Marionette->new(%parameters); ok($firefox->quit() == 0, "Firefox (visible => 1) exited okay when $function is reset"); File::Path::rmtree( $base_directory, 0, 0 ); done_testing(); } 1; Firefox-Marionette-1.63/t/manifest.t0000644000175000017500000000077714615647672016065 0ustar davedave#!perl -T use 5.006; use strict; use warnings FATAL => 'all'; use Test::More; unless ( $ENV{RELEASE_TESTING} ) { plan( skip_all => "Author tests not required for installation" ); } my $min_tcm = 0.9; eval "use Test::CheckManifest $min_tcm"; plan skip_all => "Test::CheckManifest $min_tcm required" if $@; ok_manifest({filter => [qr/(?: [.]git| local| cover_db| fingerprintjs| servers[.]csv| BotD| browserfeatcl| package\-lock[.]json| MYMETA[.]json[.]lock )/smx]}); Firefox-Marionette-1.63/t/03-seek.t0000644000175000017500000000070714547652665015421 0ustar davedave#! /usr/bin/perl -w use strict; use lib qw(t/); use syscall_tests (qw(seek)); *CORE::GLOBAL::seek = sub { if (syscall_tests::allow()) { return CORE::seek $_[0], $_[1], $_[2]; } else { $! = POSIX::ESPIPE(); return } }; require Firefox::Marionette; syscall_tests::run(POSIX::ESPIPE()); syscall_tests::visible(POSIX::ESPIPE()); no warnings; *CORE::GLOBAL::seek = sub { return CORE::seek $_[0], $_[1], $_[2]; }; use warnings; syscall_tests::finalise(); Firefox-Marionette-1.63/t/04-proxy.t0000644000175000017500000003324414711620626015637 0ustar davedave#! /usr/bin/perl -w use strict; use Firefox::Marionette(); use Test::More; use File::Spec(); use MIME::Base64(); use Socket(); use Config; use Crypt::URandom(); use lib qw(t/); $SIG{INT} = sub { die "Caught an INT signal"; }; $SIG{TERM} = sub { die "Caught a TERM signal"; }; SKIP: { if (!$ENV{RELEASE_TESTING}) { plan skip_all => "Author tests not required for installation"; } if ($^O eq 'MSWin32') { plan skip_all => "Cannot test in a $^O environment"; } require test_daemons; if (!Test::CA->available()) { plan skip_all => "openssl does not appear to be available"; } my $default_rsa_key_size = 4096; if (!Test::Daemon::Nginx->available()) { plan skip_all => "nginx does not appear to be available"; } if (!Test::Daemon::Squid->available()) { plan skip_all => "squid does not appear to be available"; } my $override_address = $^O eq 'linux' ? undef : '127.0.0.1'; my $jumpd_listen = 'localhost'; if (!Test::Daemon::SSH->connect_and_exit($jumpd_listen)) { plan skip_all => "Cannot login to $jumpd_listen with ssh"; } my $sshd_listen = $override_address || '127.0.0.3'; if (!Test::Daemon::SSH->connect_and_exit($sshd_listen)) { plan skip_all => "Cannot login to $sshd_listen with ssh"; } my $ca = Test::CA->new($default_rsa_key_size); my $squid_listen = $override_address || '127.0.0.4'; my $nginx_listen = $override_address || '127.0.0.5'; my $socks_listen = $override_address || '127.0.0.6'; my $nginx_username = MIME::Base64::encode_base64( Crypt::URandom::urandom( 50 ), q[] ); my $nginx_password = MIME::Base64::encode_base64( Crypt::URandom::urandom( 100 ), q[] ); my $nginx_realm = "Nginx Server for Firefox::Marionette $0"; my $nginx = Test::Daemon::Nginx->new(listen => $nginx_listen, key_size => $default_rsa_key_size, ca => $ca, $ENV{FIREFOX_NO_WEB_AUTH} ? () : ( username => $nginx_username, password => $nginx_password, realm => $nginx_realm)); ok($nginx, "Started nginx Server on $nginx_listen on port " . $nginx->port() . ", with pid " . $nginx->pid()); $nginx->wait_until_port_open(); my $squid_username = MIME::Base64::encode_base64( Crypt::URandom::urandom( 50 ), q[] ); my $squid_password = MIME::Base64::encode_base64( Crypt::URandom::urandom( 100 ), q[] ); my $squid_realm = "Squid Proxy for Firefox::Marionette $0"; my $squid = Test::Daemon::Squid->new(listen => $squid_listen, key_size => $default_rsa_key_size, ca => $ca, allow_ssl_port => $nginx->port(), $ENV{FIREFOX_NO_PROXY_AUTH} ? () : (username => $squid_username, password => $squid_password, realm => $squid_realm)); ok($squid, "Started squid Server on $squid_listen on port " . $squid->port() . ", with pid " . $squid->pid()); $squid->wait_until_port_open(); my $profile = Firefox::Marionette::Profile->new(); $profile->set_value( 'network.proxy.allow_hijacking_localhost', 'true', 0 ); my $debug = $ENV{FIREFOX_DEBUG} || 0; my $visible = $ENV{FIREFOX_VISIBLE} || 0; my $loop_max = $ENV{FIREFOX_MAX_LOOP} || 1; AUTH_LOOP: foreach my $loop_count (1 .. $loop_max) { my $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, profile => $profile, host => "$sshd_listen:22", via => "$jumpd_listen:22", trust => $ca->cert(), proxy => "https://$squid_listen:" . $squid->port() ); ok($firefox, "Created a firefox object going through ssh and web proxies"); if (!$ENV{FIREFOX_NO_PROXY_AUTH}) { ok($firefox->add_login(host => "moz-proxy://$squid_listen:" . $squid->port(), user => $squid_username, password => $squid_password, realm => $squid_realm), "Added proxy credentials"); } if (!$ENV{FIREFOX_NO_WEB_AUTH}) { ok($firefox->add_login(host => "https://$nginx_listen:" . $nginx->port(), user => $nginx_username, password => $nginx_password, realm => $nginx_realm), "Added web server credentials"); } eval { if ($ENV{FIREFOX_NO_WEB_AUTH}) { ok($firefox->go("https://$nginx_listen:" . $nginx->port()), "Retrieved webpage (no web server auth)"); } else { ok($firefox->go("https://$nginx_listen:" . $nginx->port())->accept_alert()->await(sub { $firefox->loaded() }), "Retrieved webpage"); } } or do { chomp $@; diag("Did not load webpage:$@"); next AUTH_LOOP; }; my $strip = $firefox->strip(); TODO: { local $TODO = $ENV{FIREFOX_NO_WEB_AUTH} ? q[] : "Firefox can have race conditions for basic web auth"; ok($strip eq $nginx->content(), "Successfully retrieved web page through ssh and web proxies:$strip:"); if ($strip ne $nginx->content()) { next AUTH_LOOP; } } ok($firefox->quit() == 0, "Firefox closed successfully"); } ok($nginx->stop() == 0, "Stopped nginx on $nginx_listen:" . $nginx->port()); ok($squid->stop() == 0, "Stopped HTTPS proxy on $squid_listen:" . $squid->port()); $nginx = Test::Daemon::Nginx->new(listen => $nginx_listen); ok($nginx, "Started nginx Server on $nginx_listen on port " . $nginx->port() . ", with pid " . $nginx->pid()); $nginx->wait_until_port_open(); my $squid1 = Test::Daemon::Squid->new(listen => $squid_listen, allow_port => $nginx->port()); ok($squid1, "Started squid Server on $squid_listen on port " . $squid1->port() . ", with pid " . $squid1->pid()); $squid1->wait_until_port_open(); my $squid2 = Test::Daemon::Squid->new(listen => $squid_listen, key_size => $default_rsa_key_size, ca => $ca, allow_port => $nginx->port()); ok($squid2, "Started squid Server on $squid_listen on port " . $squid2->port() . ", with pid " . $squid2->pid()); $squid2->wait_until_port_open(); { local $ENV{all_proxy} = 'https://' . $squid_listen . ':' . $squid2->port(); my $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, profile => $profile, trust => $ca->cert(), ); ok($firefox, "Created a firefox object going through ssh and the all_proxy environment variable of (https://$squid_listen:" . $squid2->port() . ")"); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Retrieved webpage with all_proxy environment variable"); my $strip = $firefox->strip(); ok($strip eq $nginx->content(), "Successfully retrieved web page through all_proxy environment variable"); ok($squid2->stop() == 0, "Stopped HTTPS proxy on $squid_listen:" . $squid2->port()); ok($firefox->go("about:blank"), "Reset current webpage to about:blank"); eval { $firefox->go("http://$nginx_listen:" . $nginx->port()); }; chomp $@; ok($@, "Failed to load website when proxy specified by all_proxy environment variable is down:$@"); ok($firefox->go("about:blank"), "Reset current webpage to about:blank"); ok($squid2->start(), "Started HTTPS proxy on $squid_listen:" . $squid2->port()); $squid2->wait_until_port_open(); eval { $firefox->go("http://$nginx_listen:" . $nginx->port()); }; chomp $@; if ($@) { diag("Needed another page load to retry the restarted proxy"); } ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Retrieved webpage with all_proxy environment variable"); $strip = $firefox->strip(); ok($strip eq $nginx->content(), "Successfully retrieved web page through all_proxy environment variable"); } my $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, profile => $profile, host => "$sshd_listen:22", via => "$jumpd_listen:22", trust => $ca->cert(), proxy => [ "http://$squid_listen:" . $squid1->port(), "https://$squid_listen:" . $squid2->port ], ); ok($firefox, "Created a firefox object going through ssh and redundant ($squid_listen:" . $squid1->port() . " then $squid_listen:" . $squid2->port() . ") web proxies"); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Retrieved webpage with redundant web proxies"); my $strip = $firefox->strip(); ok($strip eq $nginx->content(), "Successfully retrieved web page through ssh and redundant web proxies:$strip:"); ok($firefox->go('about:blank'), 'Reset webpage to about:blank'); ok($squid1->stop() == 0, "Stopped primary HTTP proxy on $squid_listen:" . $squid1->port()); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Retrieved webpage with backup HTTPS proxy"); $strip = $firefox->strip(); ok($strip eq $nginx->content(), "Successfully retrieved web page through ssh and backup HTTPS proxies:$strip:"); ok($squid1->start(), "Started primary HTTP proxy on $squid_listen:" . $squid1->port()); $squid1->wait_until_port_open(); ok($squid2->stop() == 0, "Stopped backup HTTPS proxy on $squid_listen:" . $squid2->port()); eval { $firefox->go("http://$nginx_listen:" . $nginx->port()); }; chomp $@; if ($@) { ok($@ =~ /proxyConnectFailure/, "Firefox threw an exception b/c of proxy failure:$@"); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Retrieved webpage with primary HTTP proxy"); $strip = $firefox->strip(); ok($strip eq $nginx->content(), "Successfully retrieved web page through ssh and primary HTTP proxy:$strip:"); } else { $strip = $firefox->strip(); diag("No exception thrown when proxy stopped"); ok($strip eq $nginx->content(), "Successfully retrieved web page without throwing an exception through ssh and primary HTTP proxy:$strip:"); } ok($firefox->quit() == 0, "Firefox closed successfully"); $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, profile => $profile, host => "$sshd_listen:22", via => "$jumpd_listen:22", proxy => "http://$squid_listen:" . $squid1->port(), ); ok($firefox, "Created a firefox object going through ssh and http ($squid_listen:" . $squid1->port() . ") proxy"); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Retrieved webpage with HTTP proxy"); $strip = $firefox->strip(); ok($strip eq $nginx->content(), "Successfully retrieved web page through ssh and HTTP proxy:$strip:"); ok($squid1->stop() == 0, "Stopped HTTP proxy on $squid_listen:" . $squid->port()); my $socks = Test::Daemon::Socks->new(listen => $socks_listen, debug => 1); ok($socks, "Started SOCKS Server on $socks_listen on port " . $socks->port() . ", with pid " . $socks->pid()); $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, profile => $profile, host => "$sshd_listen:22", via => "$jumpd_listen:22", proxy => Firefox::Marionette::Proxy->new( socks => "$socks_listen:" . $socks->port() ), ); ok($firefox, "Created a firefox object going through ssh and SOCKS ($socks_listen:" . $socks->port() . ") proxy"); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Retrieved webpage with SOCKS proxy"); $strip = $firefox->strip(); ok($strip eq $nginx->content(), "Successfully retrieved web page through ssh and SOCKS proxy:$strip:"); $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, profile => $profile, host => "$sshd_listen:22", via => "$jumpd_listen:22", proxy => Firefox::Marionette::Proxy->new( socks => "$socks_listen:" . $socks->port(), socks_version => 5 ), ); ok($firefox, "Created a firefox object going through ssh and SOCKS ($socks_listen:" . $socks->port() . ") proxy with version specified"); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Retrieved webpage with SOCKS proxy (v5)"); $strip = $firefox->strip(); ok($strip eq $nginx->content(), "Successfully retrieved web page through ssh and SOCKS proxy (v5):$strip:"); $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, profile => $profile, host => "$sshd_listen:22", via => "$jumpd_listen:22", proxy => "socks://$socks_listen:" . $socks->port(), ); ok($firefox, "Created a firefox object going through ssh and SOCKS URI (socks://$socks_listen:" . $socks->port() . ") proxy "); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Retrieved webpage with SOCKS proxy (v5)"); $strip = $firefox->strip(); ok($strip eq $nginx->content(), "Successfully retrieved web page through ssh and SOCKS proxy (v5):$strip:"); $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, profile => $profile, host => "$sshd_listen:22", via => "$jumpd_listen:22", proxy => "socks4://$socks_listen:" . $socks->port(), ); ok($firefox, "Created a firefox object going through ssh and SOCKS URI (socks4://$socks_listen:" . $socks->port() . ") proxy "); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Retrieved webpage with SOCKS proxy (v4)"); $strip = $firefox->strip(); ok($strip eq $nginx->content(), "Successfully retrieved web page through ssh and SOCKS proxy (v4):$strip:"); $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, profile => $profile, host => "$sshd_listen:22", via => "$jumpd_listen:22", proxy => "socks5://$socks_listen:" . $socks->port(), ); ok($firefox, "Created a firefox object going through ssh and SOCKS URI (socks5://$socks_listen:" . $socks->port() . ") proxy "); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Retrieved webpage with SOCKS proxy (v5)"); $strip = $firefox->strip(); ok($strip eq $nginx->content(), "Successfully retrieved web page through ssh and SOCKS proxy (v5):$strip:"); foreach my $scheme (qw(socks socks4 socks5)) { local $ENV{all_proxy} = $scheme . '://' . $socks_listen . ':' . $socks->port(); my $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, profile => $profile, ); ok($firefox, "Created a firefox object going through ssh and the all_proxy environment variable of ($scheme://$socks_listen:" . $socks->port() . ")"); ok($firefox->go("http://$nginx_listen:" . $nginx->port()), "Retrieved webpage with all_proxy environment variable set to a scheme of $scheme"); my $strip = $firefox->strip(); ok($strip eq $nginx->content(), "Successfully retrieved web page through all_proxy environment variable set to a scheme of $scheme"); } ok($nginx->stop() == 0, "Stopped nginx on $nginx_listen:" . $nginx->port()); ok($socks->stop() == 0, "Stopped SOCKS proxy on $socks_listen:" . $socks->port()); } done_testing(); Firefox-Marionette-1.63/t/03-opendir.t0000644000175000017500000000075014547652665016130 0ustar davedave#! /usr/bin/perl -w use strict; use Archive::Zip(); use XML::Parser(); use lib qw(t/); use syscall_tests (qw(opendir)); *CORE::GLOBAL::opendir = sub { if (syscall_tests::allow()) { CORE::opendir $_[0], $_[1]; } else { $! = POSIX::EACCES(); return } }; require Firefox::Marionette; syscall_tests::run(POSIX::EACCES()); syscall_tests::visible(POSIX::EACCES()); no warnings; *CORE::GLOBAL::opendir = sub { return CORE::opendir $_[0], $_[1]; }; use warnings; syscall_tests::finalise(); Firefox-Marionette-1.63/t/03-fork.t0000644000175000017500000000077414547652665015437 0ustar davedave#! /usr/bin/perl -w use strict; use lib qw(t/); use syscall_tests (qw(fork)); *CORE::GLOBAL::fork = sub { if (syscall_tests::allow()) { CORE::fork; } else { $! = POSIX::ENOMEM(); return } }; require Firefox::Marionette; syscall_tests::run(POSIX::ENOMEM()); TODO: { local $syscall_tests::TODO = $^O eq 'MSWin32' ? "There are no fork calls in $^O": q[]; syscall_tests::visible(POSIX::ENOENT()); } no warnings; *CORE::GLOBAL::fork = sub { return CORE::fork; }; use warnings; syscall_tests::finalise(); Firefox-Marionette-1.63/t/pod-coverage.t0000755000175000017500000000110114547652665016615 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/^(download)$/, qr/^(ftp)$/, qr/^(xvfb)$/, qr/^(TO_JSON)$/, qr/^(list.*)$/, qr/^(accept_dialog)$/, qr/^(find_by_.*)$/, ] }); Firefox-Marionette-1.63/t/02-taint.t0000644000175000017500000000403314711620626015565 0ustar davedave#! /usr/bin/perl -wT use strict; use Firefox::Marionette(); use Test::More; use File::Spec(); my $dev_null = File::Spec->devnull(); my $run_taint_checks = 1; if ($^O eq 'MSWin32') { diag("Checking taint under $^O"); } else { delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; if (defined $ENV{PATH}) { $ENV{PATH} = '/usr/bin:/bin:/usr/local/bin'; if ($^O eq 'netbsd') { $ENV{PATH} .= ":/usr/pkg/bin"; } if ($^O eq 'openbsd') { $ENV{PATH} .= ":/usr/X11R6/bin/"; } if (my $pid = fork) { waitpid $pid, 0; if ($? == 0) { diag("Running taint checks with PATH set to $ENV{PATH}"); } else { diag("Unable to exec firefox with PATH set to $ENV{PATH}. No taint checks to be run"); $run_taint_checks = 0; } } elsif (defined $pid) { eval { open STDOUT, q[>], $dev_null or die "Failed to redirect STDOUT to $dev_null:$!"; open STDERR, q[>], $dev_null or die "Failed to redirect STDERR to $dev_null:$!"; exec { 'firefox' } 'firefox', '--version' or die "Failed to exec 'firefox':$!"; } or do { warn $@; }; exit 1; } } } my %parameters; if (($ENV{FIREFOX_HOST}) && ($ENV{FIREFOX_HOST} =~ /^(.*)$/smx)) { $parameters{host} = $1; diag("Overriding host to '$parameters{host}'"); if (($ENV{FIREFOX_VIA}) && ($ENV{FIREFOX_VIA} =~ /^(.*)$/smx)) { $parameters{via} = $1; } if (($ENV{FIREFOX_USER}) && ($ENV{FIREFOX_USER} =~ /^(.*)$/smx)) { $parameters{user} = $1; } elsif (($ENV{FIREFOX_HOST} eq 'localhost') && (!$ENV{FIREFOX_PORT})) { $parameters{user} = 'firefox'; } if (($ENV{FIREFOX_PORT}) && ($ENV{FIREFOX_PORT} =~ /^(\d+)$/smx)) { $parameters{port} = $1; } } if ($ENV{FIREFOX_DEBUG}) { $parameters{debug} = $ENV{FIREFOX_DEBUG}; } if ($ENV{FIREFOX_VISIBLE}) { $parameters{visible} = $ENV{FIREFOX_VISIBLE}; } SKIP: { if (!$run_taint_checks) { skip("Unable to run firefox with PATH set to $ENV{PATH}", 2); } my $firefox = Firefox::Marionette->new(%parameters); ok($firefox, "Firefox launched okay under taint"); ok($firefox->quit() == 0, "Firefox exited okay under taint"); } done_testing(); Firefox-Marionette-1.63/t/03-read.t0000644000175000017500000000113014547652665015374 0ustar davedave#! /usr/bin/perl -w use strict; use JSON(); BEGIN { if (($^O eq 'cygwin') || ($^O eq 'darwin') || ($^O eq 'MSWin32')) { } else { require Crypt::URandom; require FileHandle; } } use lib qw(t/); use syscall_tests (qw(read)); *CORE::GLOBAL::read = sub { if (syscall_tests::allow()) { CORE::read $_[0], $_[1], $_[2]; } else { $! = POSIX::EACCES(); return } }; require Firefox::Marionette; syscall_tests::run(POSIX::EACCES()); syscall_tests::visible(POSIX::EACCES()); no warnings; *CORE::GLOBAL::read = sub { return CORE::read $_[0], $_[1], $_[2]; }; use warnings; syscall_tests::finalise(); Firefox-Marionette-1.63/t/03-stat.t0000644000175000017500000000113314547652665015437 0ustar davedave#! /usr/bin/perl -w use strict; use Archive::Zip(); use lib qw(t/); use syscall_tests (qw(stat)); *CORE::GLOBAL::stat = sub { if (syscall_tests::allow()) { CORE::stat $_[0]; } else { $! = POSIX::ENOENT(); return } }; require Firefox::Marionette; syscall_tests::run(POSIX::ENOENT()); TODO: { local $syscall_tests::TODO = (($^O eq 'darwin') or ($^O eq 'MSWin32') or ($^O eq 'cygwin')) ? "There are no stat calls when $^O firefox starts": q[]; syscall_tests::visible(POSIX::ENOENT()); } no warnings; *CORE::GLOBAL::stat = sub { return CORE::stat $_[0]; }; use warnings; syscall_tests::finalise(); Firefox-Marionette-1.63/t/stub.pl0000755000175000017500000001735114615647672015403 0ustar davedave#! /usr/bin/env perl use strict; use warnings; use Getopt::Long(); use File::Spec(); use FileHandle(); use Fcntl(); use Socket(); use JSON(); MAIN: { my %options; Getopt::Long::GetOptions(\%options, 'version', 'marionette', 'headless', 'profile:s', 'no-remote', 'new-instance', 'devtools', 'safe-mode'); my $browser_version = "112.0.2"; if ($options{version}) { $| = 1; print "Mozilla Firefox $browser_version\n"; exit 0; } socket my $server, Socket::PF_INET(), Socket::SOCK_STREAM(), 0 or die "Failed to create a socket:$!"; bind $server, Socket::sockaddr_in( 0, Socket::INADDR_LOOPBACK() ) or die "Failed to bind socket:$!"; listen $server, Socket::SOMAXCONN() or die "Failed to listen:$!"; my $port = ( Socket::sockaddr_in( getsockname $server ) )[0]; my $prefs_path = File::Spec->catfile($options{profile}, 'prefs.js'); my $old_prefs_handle = FileHandle->new($prefs_path, Fcntl::O_RDONLY()) or die "Failed to open $prefs_path for reading:$!"; my $new_prefs_path = File::Spec->catfile($options{profile}, 'prefs.new'); my $new_prefs_handle = FileHandle->new($new_prefs_path, Fcntl::O_CREAT() | Fcntl::O_EXCL() | Fcntl::O_WRONLY(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR()) or die "Failed to open $new_prefs_path for writing:$!"; while(my $line = <$old_prefs_handle>) { if ($line =~ /^user_pref\("marionette.port",[ ]0\);/smx) { print {$new_prefs_handle} qq[user_pref("marionette.port", $port);\n] or die "Failed to write to $new_prefs_path:$!"; } else { print {$new_prefs_handle} $line or die "Failed to write to $new_prefs_path:$!"; } } close $new_prefs_handle or die "Failed to close $new_prefs_path:$!"; close $old_prefs_handle or die "Failed to close $prefs_path:$!"; rename $new_prefs_path, $prefs_path or die "Failed to rename $new_prefs_path to $prefs_path:$!"; my $paddr = accept(my $client, $server); my $old = select $client; $| = 1; select $old; syswrite $client, qq[50:{"applicationType":"gecko","marionetteProtocol":3}] or die "Failed to write to socket:$!"; my $request = _get_request($client); my $platform = $^O; my $headless = $options{headless} ? 'true' : 'false'; my $response_type = 1; my $profile_path = $options{profile}; $profile_path =~ s/\\/\\\\/smxg; my $capabilities = qq([1,1,null,{"sessionId":"5a5f9a08-0faa-4794-aa85-ee85980ce422","capabilities":{"browserName":"firefox","browserVersion":"$browser_version","platformName":"$platform","acceptInsecureCerts":false,"pageLoadStrategy":"normal","setWindowRect":true,"timeouts":{"implicit":0,"pageLoad":300000,"script":30000},"strictFileInteractability":false,"unhandledPromptBehavior":"dismiss and notify","moz:accessibilityChecks":false,"moz:buildID":"20230427144338","moz:headless":$headless,"moz:platformVersion":"6.2.14-200.fc37.x86_64","moz:processID":$$,"moz:profile":"$profile_path","moz:shutdownTimeout":60000,"moz:useNonSpecCompliantPointerOrigin":false,"moz:webdriverClick":true,"moz:windowless":false,"proxy":{}}}]); my $capability_length = length $capabilities; syswrite $client, $capability_length . q[:] . $capabilities or die "Failed to write to socket:$!"; my $context = "content"; my $addon_number = 0; while(1) { $request = _get_request($client); my $message_id = $request->[1]; if ($request->[2] eq 'Marionette:Quit') { my $response_body = qq([$response_type,$message_id,null,{"cause":"shutdown","forced":false,"in_app":true}]); _send_response_body($client, $response_body); last; } elsif ($request->[2] eq 'Addon:Install') { $addon_number += 1; my $response_body = qq([$response_type,$message_id,null,{"value":"6eea9fdc37a5d8fbcbbecd57ee7272669e828a3${addon_number}\@temporary-addon"}]); _send_response_body($client, $response_body); } elsif ($request->[2] eq 'Addon:Uninstall') { $addon_number += 1; my $response_body = qq([$response_type,$message_id,null,{"value":null}]); _send_response_body($client, $response_body); } elsif ($request->[2] eq 'WebDriver:Print') { syswrite $client, qq(1475:[$response_type,$message_id,null,{"value":"JVBERi0xLjUKJbXtrvsKNCAwIG9iago8PCAvTGVuZ3RoIDUgMCBSCiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCj4+CnN0cmVhbQp4nDNUMABCXUMgYW5ppJCcy1XIFahQyGVkoWdsaqQApUxNTfWMDQwVzI0hdFGqQrhCHpehAggWpSvoJxoopBcT1pTGFcgFACsfF2cKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCiAgIDc1CmVuZG9iagozIDAgb2JqCjw8CiAgIC9FeHRHU3RhdGUgPDwKICAgICAgL2EwIDw8IC9DQSAxIC9jYSAxID4+CiAgID4+Cj4+CmVuZG9iago2IDAgb2JqCjw8IC9UeXBlIC9PYmpTdG0KICAgL0xlbmd0aCA3IDAgUgogICAvTiAxCiAgIC9GaXJzdCA0CiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCj4+CnN0cmVhbQp4nD3NMQvCMBQE4L2/4hbnJlEUIXRoC8VBkOgmDiU+pEsSkkbsvzeJ1PG+d7wTYJWUqG+LI9SX8UXYgFdADp7MDA4GVeBMz2ls7Qf3RAx7LnA4CjzKsbNmTvWA3b8/eBsdpMwh599G0ZWuSf1ogstbeln5hNlHWlOXWj29J01qaDM2TfmvKNjoNQVsy2biL4KVMvQKZW5kc3RyZWFtCmVuZG9iago3IDAgb2JqCiAgIDE0NwplbmRvYmoKOCAwIG9iago8PCAvVHlwZSAvT2JqU3RtCiAgIC9MZW5ndGggMTEgMCBSCiAgIC9OIDMKICAgL0ZpcnN0IDE2CiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCj4+CnN0cmVhbQp4nE2PTQvCMBBE7/kVc7NFaHZrxQ+kF8WLCCLexEOosQZKt6QR1F+vRgSvs/OWNwxSM4xJMYGnrBYL6MOjs9A7U9teAdAbd+5xRA7CHqcYLeXWBrAqy0jsvJxvlfVIKuO8gDOeZAWSawhdP9c6prU33dVVfSa+TtPvG29NkDe2ladrGoO18/Yi97+rk3ZlgkWymueUj2jMIybiohgyDYjSn8JXemmCaaSOeBwA/lh/Si9o4j6UCmVuZHN0cmVhbQplbmRvYmoKMTEgMCBvYmoKICAgMTgxCmVuZG9iagoxMiAwIG9iago8PCAvVHlwZSAvWFJlZgogICAvTGVuZ3RoIDU3CiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCiAgIC9TaXplIDEzCiAgIC9XIFsxIDIgMl0KICAgL1Jvb3QgMTAgMCBSCiAgIC9JbmZvIDkgMCBSCj4+CnN0cmVhbQp4nBXKQQ0AIAwEwW1LCLyQgB1cIA8TeIPrZ7K5HPCe08CpYNxkJEdYEd6TmZemEG6xtMWGD8f2BIAKZW5kc3RyZWFtCmVuZG9iagpzdGFydHhyZWYKODYzCiUlRU9GCg=="}]) or die "Failed to write to socket:$!"; } elsif ($request->[2] eq 'WebDriver:TakeScreenshot') { syswrite $client, qq(423:[$response_type,$message_id,null,{"value":"iVBORw0KGgoAAAANSUhEUgAABVYAAAAICAYAAAAShaQyAAAA8UlEQVR4Xu3YsQ0AIAwEMbL/0ICYgOud+isr1c2+txwBAgQIECBAgAABAgQIECBAgAABAgQIfAuMsPptZUiAAAECBAgQIECAAAECBAgQIECAAIEnIKx6BAIECBAgQIAAAQIECBAgQIAAAQIECEQBYTWCmRMgQIAAAQIECBAgQIAAAQIECBAgQEBY9QMECBAgQIAAAQIECBAgQIAAAQIECBCIAsJqBDMnQIAAAQIECBAgQIAAAQIECBAgQICAsOoHCBAgQIAAAQIECBAgQIAAAQIECBAgEAWE1QhmToAAAQIECBAgQIAAAQIECBAgQIAAgQMdMh/pgqHYUwAAAABJRU5ErkJggg=="}]) or die "Failed to write to socket:$!"; } elsif ($request->[2] eq 'Marionette:GetContext') { my $response_body = qq([$response_type,$message_id,null,{"value":"$context"}]); _send_response_body($client, $response_body); } elsif ($request->[2] eq 'Marionette:SetContext') { my $response_body = qq([$response_type,$message_id,null,{"value":null}]); _send_response_body($client, $response_body); } elsif ($request->[2] eq 'WebDriver:ExecuteScript') { my $response_body; if ($request->[3]->{script} eq 'return navigator.userAgent') { my $trimmed_browser_version = $browser_version; $trimmed_browser_version =~ s/^(\d+(?:[.]\d+)?).*$/$1/smx; $response_body = qq([$response_type,$message_id,null,{"value":"Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/$trimmed_browser_version"}]); } else { my $now = time; $response_body = qq([$response_type,$message_id,null,{"value":{"guid":"root________","index":0,"type":2,"title":"","dateAdded":$now,"lastModified":$now,"childCount":5}}]); } _send_response_body($client, $response_body); } else { die "Unsupported method in stub firefox"; } } close $client or die "Failed to close socket:$!"; exit 0; } sub _send_response_body { my ($client, $response_body) = @_; my $response_length = length $response_body; syswrite $client, qq(${response_length}:$response_body) or die "Failed to write to socket:$!"; } sub _get_request { my ($client) = @_; my $length_buffer = q[]; sysread $client, my $buffer, 1 or die "Failed to read from socket:$!"; while($buffer ne q[:]) { $length_buffer .= $buffer; sysread $client, $buffer, 1 or die "Failed to read from socket:$!"; } sysread $client, $buffer, $length_buffer or die "Failed to read from socket:$!"; my $request = JSON->new()->utf8()->decode($buffer); return $request; } Firefox-Marionette-1.63/t/04-botd.t0000644000175000017500000001261114633562513015404 0ustar davedave#! /usr/bin/perl -w use strict; use Firefox::Marionette(); use Test::More; use File::Spec(); use lib qw(t/); $SIG{INT} = sub { die "Caught an INT signal"; }; $SIG{TERM} = sub { die "Caught a TERM signal"; }; SKIP: { if (!$ENV{RELEASE_TESTING}) { plan skip_all => "Author tests not required for installation"; } if ($^O eq 'MSWin32') { plan skip_all => "Cannot test in a $^O environment"; } require test_daemons; if (!Test::Daemon::Botd->botd_available()) { plan skip_all => "BotD does not appear to be available"; } if (!Test::Daemon::Botd->available()) { plan skip_all => "yarn does not appear to be available in $ENV{PATH}"; } my $override_address = $^O eq 'linux' ? undef : '127.0.0.1'; my $botd_listen = $override_address || '127.0.0.2'; my $botd = Test::Daemon::Botd->new(listen => $botd_listen); ok($botd, "Started botd Server on $botd_listen on port " . $botd->port() . ", with pid " . $botd->pid()); $botd->wait_until_port_open(); my $debug = $ENV{FIREFOX_DEBUG} || 0; my $visible = $ENV{FIREFOX_VISIBLE} || 0; my $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, ); ok($firefox, "Created a firefox object in normal mode"); ok(_am_i_a_bot($firefox, $botd), "BotD did detect a bot"); my %correct_values = _get_property_descriptors($firefox); foreach my $property (sort { $a cmp $b } keys %correct_values) { foreach my $descriptor (sort { $a cmp $b } keys %{$correct_values{$property}}) { ok(1, "navigator.$property ($descriptor) = $correct_values{$property}{$descriptor}"); } } ok($firefox->quit() == 0, "Firefox closed successfully"); $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, stealth => 1, ); ok($firefox, "Created a firefox object in stealth mode"); ok(!_am_i_a_bot($firefox, $botd), "BotD did NOT detect a bot"); my %actual_values = _get_property_descriptors($firefox); foreach my $property (sort { $a cmp $b } keys %correct_values) { foreach my $descriptor (sort { $a cmp $b } keys %{$correct_values{$property}}) { ok($correct_values{$property}{$descriptor} eq $actual_values{$property}{$descriptor}, "navigator.$property ($descriptor) = $correct_values{$property}{$descriptor}:$actual_values{$property}{$descriptor}"); } } my $json = JSON::decode_json($firefox->find_id('debug-data')->text()); ok($json->{browserKind} eq 'firefox', "BotD reports browserKind of 'firefox':$json->{browserKind}"); ok($json->{browserEngineKind} eq 'gecko', "BotD reports browserEngineKind of 'gecko':$json->{browserEngineKind}"); ok($firefox->quit() == 0, "Firefox closed successfully"); foreach my $agent ( q[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36], q[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15], ) { $firefox = Firefox::Marionette->new( debug => $debug, visible => $visible, stealth => 1, ); ok($firefox, "Created a firefox object in stealth mode"); ok($firefox->agent($agent), "Setting user agent to '$agent'"); ok(!_am_i_a_bot($firefox, $botd), "BotD did NOT detect a bot"); my %actual_values = _get_property_descriptors($firefox); foreach my $property (sort { $a cmp $b } keys %correct_values) { foreach my $descriptor (sort { $a cmp $b } keys %{$correct_values{$property}}) { if (defined $actual_values{$property}{$descriptor}) { my $correct_value = $correct_values{$property}{$descriptor}; if ($agent =~ /Chrome/smx) { $correct_value = qq[function $property() { [native code] }]; } else { $correct_value = qq[function $property() {\\n [native code]\\n}]; } ok($actual_values{$property}{$descriptor} eq $correct_value, "navigator.$property ($descriptor) = $correct_value:$actual_values{$property}{$descriptor}"); } } } $json = JSON::decode_json($firefox->find_id('debug-data')->text()); if ($agent =~ /Chrome/smx) { ok($json->{browserKind} eq 'chrome', "BotD reports browserKind of 'chrome':$json->{browserKind}"); ok($json->{browserEngineKind} eq 'chromium', "BotD reports browserEngineKind of 'chromium':$json->{browserEngineKind}"); } else { ok($json->{browserKind} eq 'safari', "BotD reports browserKind of 'safari':$json->{browserKind}"); ok($json->{browserEngineKind} eq 'webkit', "BotD reports browserEngineKind of 'webkit':$json->{browserEngineKind}"); } ok($firefox->quit() == 0, "Firefox closed successfully"); } ok($botd->stop() == 0, "Stopped botd on $botd_listen:" . $botd->port()); } sub _am_i_a_bot { my ($firefox, $botd) = @_; my $bot_not_detected_string = 'You are not a bot.'; ok($firefox->go(q[http://] . $botd->address() . q[:] . $botd->port()), q[Retrieved botd page from http://] . $botd->address() . q[:] . $botd->port()); my $result = $firefox->find_id('result-text')->text(); return $result ne $bot_not_detected_string; } sub _get_property_descriptors { my ($firefox) = @_; my %values; foreach my $property (qw(vendor vendorSub productSub oscpu)) { foreach my $descriptor (qw(get)) { if (defined $firefox->script("return Object.getOwnPropertyDescriptor(window.Navigator.prototype, '$property')")) { $values{$property}{$descriptor} = $firefox->script("return Object.getOwnPropertyDescriptor(window.Navigator.prototype, '$property').$descriptor.toString()"); $values{$property}{$descriptor} =~ s/\n/\\n/smxg; } } } return %values; } done_testing(); Firefox-Marionette-1.63/t/addons/0000755000175000017500000000000014763402246015315 5ustar davedaveFirefox-Marionette-1.63/t/addons/borderify/0000755000175000017500000000000014763402246017302 5ustar davedaveFirefox-Marionette-1.63/t/addons/borderify/manifest.json0000644000175000017500000000065314552454345022012 0ustar davedave{ "description": "Adds a solid red border to all webpages matching mozilla.org. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#borderify", "manifest_version": 2, "name": "Borderify", "version": "1.0", "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/borderify", "content_scripts": [ { "matches": [""], "js": ["borderify.js"] } ] } Firefox-Marionette-1.63/t/addons/borderify/borderify.js0000644000175000017500000000014014552454345021623 0ustar davedave/* Just draw a border round the document.body. */ document.body.style.border = "5px solid red"; Firefox-Marionette-1.63/t/addons/test.xpi0000755000175000017500000000103414547652665017033 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.63/t/addons/discogs-search/0000755000175000017500000000000014763402246020213 5ustar davedaveFirefox-Marionette-1.63/t/addons/discogs-search/manifest.json0000644000175000017500000000101514547652665022725 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.63/t/addons/discogs-search/README.md0000644000175000017500000000076114547652665021512 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.63/t/04-timezone.t0000644000175000017500000002263414763400572016314 0ustar davedave#! /usr/bin/perl -w use strict; use Firefox::Marionette(); use Test::More; use File::Spec(); use MIME::Base64(); use Socket(); use Config; use Crypt::URandom(); use Time::Local(); use lib qw(t/); $SIG{INT} = sub { die "Caught an INT signal"; }; $SIG{TERM} = sub { die "Caught a TERM signal"; }; SKIP: { if (!$ENV{RELEASE_TESTING}) { plan skip_all => "Author tests not required for installation"; } if ($^O eq 'MSWin32') { plan skip_all => "Cannot test in a $^O environment"; } require test_daemons; if (!Test::Daemon::Nginx->available()) { plan skip_all => "nginx does not appear to be available"; } my $nginx_listen = '127.0.0.1'; my $htdocs = File::Spec->catdir(Cwd::cwd(), 't', 'data'); my $nginx = Test::Daemon::Nginx->new(listen => $nginx_listen, htdocs => $htdocs, index => 'timezone.html'); ok($nginx, "Started nginx Server on " . $nginx->address() . " on port " . $nginx->port() . ", with pid " . $nginx->pid()); $nginx->wait_until_port_open(); my $debug = $ENV{FIREFOX_DEBUG} || 0; my $visible = $ENV{FIREFOX_VISIBLE} || 0; my %extra_parameters; if ($ENV{FIREFOX_BINARY}) { $extra_parameters{binary} = $ENV{FIREFOX_BINARY}; } local $ENV{LANG} = 'en-US'; my %ipgeolocation_responses = ( 'America/New_York' => 'data:application/json,{"ip":"72.229.28.185","continent_code":"NA","continent_name":"North America","country_code2":"US","country_code3":"USA","country_name":"United States","country_name_official":"United States of America","country_capital":"Washington, D.C.","state_prov":"New York","state_code":"US-NY","district":"","city":"New York","zipcode":"10014","latitude":"40.73661","longitude":"-74.00945","is_eu":false,"calling_code":"+1","country_tld":".us","languages":"en-US,es-US,haw,fr","country_flag":"https://ipgeolocation.io/static/flags/us_64.png","geoname_id":"6343019","isp":"Charter Communications Inc","connection_type":"","organization":"Charter Communications Inc","country_emoji":"\uD83C\uDDFA\uD83C\uDDF8","currency":{"code":"USD","name":"US Dollar","symbol":"$"},"time_zone":{"name":"America/New_York","offset":-5,"offset_with_dst":-4,"current_time":"2024-05-24 17:36:16.869-0400","current_time_unix":1716586576.869,"is_dst":true,"dst_savings":1,"dst_exists":true,"dst_start":{"utc_time":"2024-03-10 TIME 07","duration":"+1H","gap":true,"dateTimeAfter":"2024-03-10 TIME 03","dateTimeBefore":"2024-03-10 TIME 02","overlap":false},"dst_end":{"utc_time":"2024-11-03 TIME 06","duration":"-1H","gap":false,"dateTimeAfter":"2024-11-03 TIME 01","dateTimeBefore":"2024-11-03 TIME 02","overlap":true}}}', 'Australia/Adelaide' => 'data:application/json,{"ip":"58.174.22.52","continent_code":"OC","continent_name":"Oceania","country_code2":"AU","country_code3":"AUS","country_name":"Australia","country_name_official":"Commonwealth of Australia","country_capital":"Canberra","state_prov":"South Australia","state_code":"AU-SA","district":"","city":"Adelaide","zipcode":"5000","latitude":"-34.92585","longitude":"138.59980","is_eu":false,"calling_code":"+61","country_tld":".au","languages":"en-AU","country_flag":"https://ipgeolocation.io/static/flags/au_64.png","geoname_id":"8828296","isp":"Telstra Limited","connection_type":"","organization":"Telstra Corporation","country_emoji":"\uD83C\uDDE6\uD83C\uDDFA","currency":{"code":"AUD","name":"Australian Dollar","symbol":"A$"},"time_zone":{"name":"Australia/Adelaide","offset":9.5,"offset_with_dst":9.5,"current_time":"2024-05-25 07:07:02.397+0930","current_time_unix":1716586622.397,"is_dst":false,"dst_savings":0,"dst_exists":true,"dst_start":{"utc_time":"2024-10-05 TIME 16","duration":"+1H","gap":true,"dateTimeAfter":"2024-10-06 TIME 03","dateTimeBefore":"2024-10-06 TIME 02","overlap":false},"dst_end":{"utc_time":"2025-04-05 TIME 16","duration":"-1H","gap":false,"dateTimeAfter":"2025-04-06 TIME 02","dateTimeBefore":"2025-04-06 TIME 03","overlap":true}}}', 'Africa/Cairo' => 'data:application/json,{"ip":"197.246.34.1","continent_code":"AF","continent_name":"Africa","country_code2":"EG","country_code3":"EGY","country_name":"Egypt","country_name_official":"Arab Republic of Egypt","country_capital":"Cairo","state_prov":"Cairo Governorate","state_code":"EG-C","district":"","city":"Cairo","zipcode":"4460331","latitude":"30.10007","longitude":"31.33265","is_eu":false,"calling_code":"+20","country_tld":".eg","languages":"ar-EG,en,fr","country_flag":"https://ipgeolocation.io/static/flags/eg_64.png","geoname_id":"8025391","isp":"NOOR","connection_type":"","organization":"NOOR_as20928","country_emoji":"\uD83C\uDDEA\uD83C\uDDEC","currency":{"code":"EGP","name":"Egyptian Pound","symbol":"E£"},"time_zone":{"name":"Africa/Cairo","offset":2,"offset_with_dst":3,"current_time":"2024-05-31 13:54:09.966+0300","current_time_unix":1717152849.966,"is_dst":true,"dst_savings":1,"dst_exists":true,"dst_start":{"utc_time":"2024-04-25 TIME 22","duration":"+1H","gap":true,"dateTimeAfter":"2024-04-26 TIME 01","dateTimeBefore":"2024-04-26 TIME 00","overlap":false},"dst_end":{"utc_time":"2024-10-31 TIME 21","duration":"-1H","gap":false,"dateTimeAfter":"2024-10-31 TIME 23","dateTimeBefore":"2024-11-01 TIME 00","overlap":true}}}', 'Asia/Tokyo' => 'data:application/json,{"ip":"103.27.184.54","continent_code":"AS","continent_name":"Asia","country_code2":"JP","country_code3":"JPN","country_name":"Japan","country_name_official":"Japan","country_capital":"Tokyo","state_prov":"Tokyo","state_code":"JP-13","district":"","city":"Tokyo","zipcode":"135-0021","latitude":"35.68408","longitude":"139.80885","is_eu":false,"calling_code":"+81","country_tld":".jp","languages":"ja","country_flag":"https://ipgeolocation.io/static/flags/jp_64.png","geoname_id":"6526229","isp":"gbpshk.com","connection_type":"","organization":"Starry Network Limited","country_emoji":"\uD83C\uDDEF\uD83C\uDDF5","currency":{"code":"JPY","name":"Yen","symbol":"Â¥"},"time_zone":{"name":"Asia/Tokyo","offset":9,"offset_with_dst":9,"current_time":"2024-05-30 05:51:47.239+0900","current_time_unix":1717015907.239,"is_dst":false,"dst_savings":0,"dst_exists":false,"dst_start":"","dst_end":""}}', ); my @ids = qw( locale unixTime toString toLocaleString getDate getDay getMonth getFullYear getHours getMinutes getSeconds Collator DisplayNames DurationFormat ListFormat PluralRules RelativeTimeFormat Segmenter ); my $locale_diag; my $now = time; foreach my $timezone (sort { $a cmp $b } keys %ipgeolocation_responses) { ok($timezone, "Using timezone '$timezone'"); my %correct_answers; { local $ENV{TZ} = $timezone; my $firefox = Firefox::Marionette->new( %extra_parameters, debug => $debug, visible => $visible, ); ok($firefox, "Created a normal firefox object"); foreach my $time ( Time::Local::timegm(1,0,0,1,0,2000), Time::Local::timegm(59,59,23,31,11,2010), Time::Local::timegm(5,5,5,5,5,2005), ) { ok($time, "Current time in $timezone is " . localtime $time); my $url = 'http://' . $nginx->address() . q[:] . $nginx->port() . '#' . $time; ok($firefox->go('about:blank'), "reset url to about:blank"); ok($firefox->go($url), "go to $url with TZ set to $timezone"); foreach my $id (@ids) { $correct_answers{$time}{$id} = $firefox->find_id($id)->text(); if (($id eq 'locale') && (!$locale_diag)) { diag("Using locale of '$correct_answers{$time}{$id}'"); $locale_diag = 1; } ok(defined $correct_answers{$time}{$id}, "\$firefox->find_id('$id')->text() is '$correct_answers{$time}{$id}' in $timezone"); } } $firefox->quit(); } my $firefox = Firefox::Marionette->new( %extra_parameters, debug => $debug, visible => $visible, geo => $ipgeolocation_responses{$timezone}, ); ok($firefox, "Created a normal firefox object"); foreach my $time (sort { $a <=> $b } keys %correct_answers) { ok($time, "Current time in $timezone is " . localtime $time); my $url = 'http://' . $nginx->address() . q[:] . $nginx->port() . '#' . $time; ok($firefox->go('about:blank'), "reset url to about:blank"); ok($firefox->go($url), "go to $url with geo timezone set to $timezone"); my ($major, $minor, $patch) = split /[.]/smx, $firefox->browser_version(); foreach my $id (@ids) { TODO: { local $TODO = $id eq 'DurationFormat' && $major > 135 ? "$id has started failing after Firefox 135" : q[]; my $actual_answer = $firefox->find_id($id)->text(); ok($correct_answers{$time}{$id} eq $actual_answer, "\$firefox->find_id('$id')->text() returned '$correct_answers{$time}{$id}':'$actual_answer'"); $actual_answer = $firefox->find_id('iframe_' . $id)->text(); ok($correct_answers{$time}{$id} eq $actual_answer, "\$firefox->find_id('iframe_$id')->text() returned '$correct_answers{$time}{$id}':'$actual_answer'"); } } } $timezone = 'Australia/Melbourne'; ok($firefox->tz($timezone), "\$firefox->tz(\"$timezone\") is called to override the timezone"); my $url = 'http://' . $nginx->address() . q[:] . $nginx->port() . '#' . $now; ok($firefox->go('about:blank'), "reset url to about:blank"); ok($firefox->go($url), "go to $url with timezone set to $timezone"); my $id = 'toString'; my $override_answer = $firefox->find_id($id)->text(); ok($override_answer =~ /GMT[+]1[10]00/smx, "\$firefox->find_id('$id')->text() returned an answer matching Melbourne:'$override_answer'"); ok($firefox->quit() == 0, "\$firefox->quit() succeeded"); } ok($nginx->stop() == 0, "Stopped nginx on " . $nginx->address() . q[:] . $nginx->port()); } done_testing(); Firefox-Marionette-1.63/t/01-marionette.t0000755000175000017500000115110514763400572016626 0ustar davedave#! /usr/bin/perl use strict; use warnings; use Digest::SHA(); use MIME::Base64(); use Test::More; use Cwd(); use Encode(); use Firefox::Marionette(); use Waterfox::Marionette(); use Compress::Zlib(); use IO::Socket::IP(); use Config; use HTTP::Daemon(); use HTTP::Status(); use HTTP::Response(); use IO::Socket::SSL(); use File::HomeDir(); BEGIN: { if ( $^O eq 'MSWin32' ) { require Win32::Process; } } my $segv_detected; my $at_least_one_success; my $terminated; my $class; my $arch_32bit_re = qr/^(?:x86|arm(?:hf|el)?)$/smxi; my $quoted_home_directory = quotemeta File::HomeDir->my_home(); my $is_covering = !!(eval 'Devel::Cover::get_coverage()'); my $oldfh = select STDOUT; $| = 1; select $oldfh; $oldfh = select STDERR; $| = 1; select $oldfh; if (defined $ENV{WATERFOX}) { $class = 'Waterfox::Marionette'; $class->import(qw(:all)); } else { $class = 'Firefox::Marionette'; $class->import(qw(:all)); } diag("Starting test at " . localtime); my $alarm; if (defined $ENV{FIREFOX_ALARM}) { if ($ENV{FIREFOX_ALARM} =~ /^(\d{1,6})\s*$/smx) { ($alarm) = ($1); diag("Setting the ALARM value to $alarm"); alarm $alarm; } else { die "Invalid value of FIREFOX_ALARM ($ENV{FIREFOX_ALARM})"; } } foreach my $name (qw(FIREFOX_HOST FIREFOX_USER)) { if (exists $ENV{$name}) { if (defined $ENV{$name}) { $ENV{$name} =~ s/\s*$//smx; } else { die "This is just not possible:$name"; } } } my $test_time_limit = 90; my $page_content = 'page-content'; my $form_control = 'form-control'; my $css_form_control = 'input.form-control'; my $footer_links = 'footer-links'; my $xpath_for_read_text_and_size = '//a[@class="keyboard-shortcuts"]'; my $freeipapi_uri = 'data:application/json,{"ipVersion":6,"ipAddress":"2001:8001:4ab3:d800:7215:c1fe:fc85:1329","latitude":-37.5,"longitude":144.5,"countryName":"Australia","countryCode":"AU","timeZone":"+11:00","zipCode":"3000","cityName":"Melbourne","regionName":"Victoria","isProxy":false,"continent":"Oceania","continentCode":"OC"}'; # sourced from https://freeipapi.com/api/json/ my $geocode_maps_uri = 'data:application/json,[{"place_id":18637666,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","osm_type":"node","osm_id":6173167285,"boundingbox":["-37.6","-37.4","144.4","144.5"],"lat":"-37.5","lon":"144.5","display_name":"Boston Consulting Group, 101, Collins Street, East End Theatre District, Melbourne, City of Melbourne, Victoria, 3000, Australia","class":"office","type":"company","importance":1.1674899222535406}]'; # sourced from https://geocode.maps.co/search?street=101+Collins+St&city=Melbourne&state=VIC&postalcode=3000&country=AU&format=json my $positionstack_uri = 'data:application/json,{"data":[{"latitude":-37.5,"longitude":144.5,"type":"address","name":"101 Collins Street","number":"101","postal_code":"3000","street":"Collins Street","confidence":1,"region":"Victoria","region_code":"VIC","county":"Melbourne","locality":"Melbourne","administrative_area":"Melbourne","neighbourhood":null,"country":"Australia","country_code":"AUS","continent":"Oceania","label":"101 Collins Street, Melbourne, VIC, Australia"}]}'; # source from http://api.positionstack.com/v1/forward?access_key=$API_KEY_HERE&query=101+Collins+St,Melbourne,VIC+3000 my $ipgeolocation_uri = 'data:application/json,{"ip":"2001:8001:4ab3:d800:7215:c1fe:fc85:1329","continent_code":"OC","continent_name":"Oceania","country_code2":"AU","country_code3":"AUS","country_name":"Australia","country_name_official":"Commonwealth of Australia","country_capital":"Canberra","state_prov":"Victoria","state_code":"AU-VIC","district":"","city":"Melbourne","zipcode":"3000","latitude":"-37.5","longitude":"144.5","is_eu":false,"calling_code":"+61","country_tld":".au","languages":"en-AU","country_flag":"https://ipgeolocation.io/static/flags/au_64.png","geoname_id":"2166436","isp":"Telstra Corporation Limited","connection_type":"","organization":"Telstra Corporation Limited","currency":{"code":"AUD","name":"Australian Dollar","symbol":"A$"},"time_zone":{"name":"Australia/Melbourne","offset":10,"offset_with_dst":11,"current_time":"2024-01-09 17:54:54.413+1100","current_time_unix":1704783294.413,"is_dst":true,"dst_savings":1}}'; # sourced from https://api.ipgeolocation.io/ipgeo?apiKey=$API_KEY_HERE my $ipstack_uri = 'data:application/json,{"ip": "2001:8003:4a03:d800:7285:c2ff:fe85:1528", "type": "ipv6", "continent_code": "OC", "continent_name": "Oceania", "country_code": "AU", "country_name": "Australia", "region_code": "VIC", "region_name": "Victoria", "city": "Melbourne", "zip": "3000", "latitude": -37.5, "longitude": 144.5, "location": {"geoname_id": 2158177, "capital": "Canberra", "languages": [{"code": "en", "name": "English", "native": "English"}], "country_flag": "https://assets.ipstack.com/flags/au.svg", "country_flag_emoji": "\ud83c\udde6\ud83c\uddfa", "country_flag_emoji_unicode": "U+1F1E6 U+1F1FA", "calling_code": "61", "is_eu": false}}'; my $dummy1_uri = 'data:application/json,{"latitude":40.7,"longitude":-73.9,"time_zone":{"current_time":"2024-01-09 04:36:29.524-0500"}}'; # dummy data for testing (roughly new york) my $dummy2_uri = 'data:application/json,{"latitude":40.7,"longitude":-73.9,"time_zone":{"current_time":"1234abc"}}'; # dummy data for testing bad data my $most_common_useragent = q[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36]; my $useragents_me_uri = qq[data:application/json,{"about": "Use this API to get a list of current popular useragents. Please post a link back to the site if you find it useful!", "terms": "As the data here don't change sooner than once per week, you shouldn't need to make lots of requests all at once. Currently, we impose a rate-limit of 15 requests per IP address per hour (even this is probably too many)", "data": [{"ua": "$most_common_useragent", "pct": 37.8271882916}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63", "pct": 14.2696312975}, {"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", "pct": 10.8077680833}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0", "pct": 6.5859836758}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.57", "pct": 4.9535603715}, {"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", "pct": 4.5032367014}, {"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15", "pct": 4.5032367014}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", "pct": 1.9138755981}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 OPR/95.0.0.0", "pct": 1.2383900929}, {"ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", "pct": 0.7880664227}, {"ua": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0", "pct": 0.7880664227}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.41", "pct": 0.7880664227}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.56", "pct": 0.6754855052}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36 Edg/103.0.1264.37", "pct": 0.6754855052}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0", "pct": 0.6754855052}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36 Edg/90.0.818.46", "pct": 0.5629045877}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50", "pct": 0.5629045877}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Whale/3.19.166.16 Safari/537.36", "pct": 0.5629045877}, {"ua": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0", "pct": 0.4503236701}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.76", "pct": 0.4503236701}, {"ua": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36", "pct": 0.4503236701}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.46", "pct": 0.3377427526}, {"ua": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0", "pct": 0.3377427526}, {"ua": "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko", "pct": 0.3377427526}, {"ua": "Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0", "pct": 0.3377427526}, {"ua": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Core/1.94.192.400 QQBrowser/11.5.5250.400", "pct": 0.3377427526}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.78", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 OPR/95.0.0.0", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (X11; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.61", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/110.0", "pct": 0.2251618351}, {"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.70", "pct": 0.2251618351}], "updated": 1678066547, "thanks": "https://www.useragents.me/"}]; my $min_geo_version = 60; my $min_stealth_version = 59; 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"); } foreach my $env_name ( 'XAUTHORITY', # see GH#1 'XDG_RUNTIME_DIR', # see GH#33 ) { if ( exists $ENV{$env_name} ) { delete $ENV{$env_name}; warn "Running as root. Deleting the $env_name 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 wait_for_server_on { my ($daemon, $url, $pid) = @_; my $host = URI->new($url)->host(); my $port = URI->new($url)->port(); undef $daemon; CONNECT: while (!IO::Socket::IP->new(Type => Socket::SOCK_STREAM(), PeerPort => $port, PeerHost => $host)) { diag("Waiting for server ($pid) to listen on $host:$port:$!"); waitpid $pid, POSIX::WNOHANG(); if (kill 0, $pid) { sleep 1; } else { diag("Server ($pid) has exited"); last CONNECT; } } return } sub empty_port { socket my $socket, Socket::PF_INET(), Socket::SOCK_STREAM(), 0 or die "Failed to create a socket:$!"; bind $socket, Socket::sockaddr_in( 0, Socket::INADDR_LOOPBACK() ) or die "Failed to bind socket:$!"; my $port = ( Socket::sockaddr_in( getsockname $socket ) )[0]; close $socket or die "Failed to close random socket:$!"; return $port; } sub process_alive { my ($pid) = @_; if ($^O eq 'MSWin32') { if (Win32::Process::Open(my $process, $pid, 0)) { $process->GetExitCode( my $exit_code ); if ( $exit_code == Win32::Process::STILL_ACTIVE() ) { return 1; } } else { return 0; } } else { return kill 0, $pid; } } 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 $ca_private_key_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 ($ENV{FIREFOX_FORCE_SCP}) { $parameters{scp} = 1; } 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} = [ $ca_cert_handle->filename() ]; } else { diag("Setting trust to scalar"); $parameters{trust} = $ca_cert_handle->filename(); } } if (defined $major_version) { if ($major_version >= $min_stealth_version) { # https://developer.mozilla.org/en-US/docs/Web/API/Navigator/webdriver#browser_compatibility } elsif ($parameters{stealth}) { diag("stealth support is not available for Firefox versions less than $min_stealth_version"); delete $parameters{stealth}; } if ($major_version >= $min_geo_version) { } elsif ($parameters{geo}) { diag("geo support is not available for Firefox versions less than $min_geo_version"); delete $parameters{geo}; } 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 ((defined $major_version) && ($major_version >= 60)) { } elsif ($parameters{bookmarks}) { diag("Bookmark support is not available for Firefox versions less than 60"); delete $parameters{bookmarks}; } } 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}; } my $skip_message; if ($ENV{FIREFOX_HOST}) { $parameters{host} = $ENV{FIREFOX_HOST}; diag("Overriding host to '$parameters{host}'"); if ($ENV{FIREFOX_VIA}) { $parameters{via} = $ENV{FIREFOX_VIA}; } 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 (($parameters{visible}) || ($require_visible)) { $skip_message = "Firefox visible tests are unreliable on a remote host"; return ($skip_message, undef); } } 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; $parameters{visible} = $require_visible; 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"); } elsif ($ENV{FIREFOX_NO_VISIBLE}) { $parameters{visible} = 0; 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); } } else { $parameters{visible} = $require_visible; } 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; if (defined $firefox->root_directory()) { ok($firefox, "\$firefox->root_directory() is " . $firefox->root_directory()); } else { ok($firefox, "\$firefox->root_directory() is not defined yet"); } } return ($skip_message, $firefox); } umask 0; my $binary = 'firefox'; if ($ENV{FIREFOX_BINARY}) { $binary = $ENV{FIREFOX_BINARY}; } elsif ( $^O eq 'MSWin32' ) { foreach my $possible ( 'ProgramFiles(x86)', 'ProgramFiles' ) { if (( $ENV{$possible} ) && (-e File::Spec->catfile($ENV{$possible}, 'Mozilla Firefox', 'firefox.exe') )) { $binary = File::Spec->catfile( $ENV{$possible}, 'Mozilla Firefox', 'firefox.exe' ); last; } } } 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 ($version_string =~ /^Mozilla[ ]Firefox[ ](\d+)[.](\d+)(?:a1)?(?:[.](\d+)(?:esr)?)?$/smx) { ($major_version, $minor_version, $patch_version) = ($1, $2, $3); } 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_VIA}) && (defined $ENV{FIREFOX_VIA})) { diag("FIREFOX_VIA is $ENV{FIREFOX_VIA}"); } 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 ($ENV{RELEASE_TESTING}) { 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 => '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 missing.example.org"); } } my $skip_message; my $profiles_work = 1; SKIP: { if ($ENV{FIREFOX_BINARY}) { skip("No profile testing when the FIREFOX_BINARY override is used", 6); } if (($ENV{WATERFOX}) || ($ENV{WATERFOX_VIA_FIREFOX})) { skip("No profile testing when any WATERFOX override is used", 6); } if ($ENV{FIREFOX_DEVELOPER}) { skip("No profile testing when the FIREFOX_DEVELOPER override is used", 6); } if ($ENV{FIREFOX_NIGHTLY}) { skip("No profile testing when the FIREFOX_NIGHTLY 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'); $profiles_work = 0; ($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"); if ($major_version < 52) { } elsif (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { } else { my $install_path = Cwd::abs_path('t/addons/test.xpi'); diag("Original install path is $install_path"); if ($^O eq 'MSWin32') { $install_path =~ s/\//\\/smxg; } diag("Installing extension from $install_path"); my $temporary = 1; my $install_id = $firefox->install($install_path, $temporary); ok($install_id, "Successfully installed an extension:$install_id"); ok($firefox->uninstall($install_id), "Successfully uninstalled an extension"); } 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); } $profile->set_value('security.webauth.webauthn_enable_softtoken', 'true', 0); ($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"); $profiles_work = 1; } } 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; my $original_agent; my $uname; my $arch; my $nightly; my $developer; 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"); $original_agent = $firefox->agent(); $uname = $firefox->uname(); $arch = $firefox->arch(); diag("Browser version is " . $capabilities->browser_version()); if ($firefox->nightly()) { $nightly = 1; diag($capabilities->browser_version() . " is a nightly release"); } if ($firefox->developer()) { $developer = 1; 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')); diag("User Agent is $original_agent"); diag("uname is $uname"); diag("Arch is $arch"); ok($uname, "Firefox is currently running in $uname"); ok($arch, "Firefox is currently running on $arch"); if ($major_version > 50) { ok($capabilities->platform_version(), "Firefox Platform version is " . $capabilities->platform_version()); } if (($^O eq 'MSWin32') || ($^O eq 'cygwin') || ($^O eq 'darwin') || ($ENV{FIREFOX_NO_UPDATE})) { if ($ENV{FIREFOX_HOST}) { diag("No update checks for $ENV{FIREFOX_HOST}"); } else { diag("No update checks for $^O"); } } elsif (($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()); $original_agent = $firefox->agent(); diag("New User Agent is $original_agent"); ($major_version, $minor_version, $patch_version) = split /[.]/smx, $update->app_version(); if ($major_version == 102) { # This was a bad firefox version for marionette. It blew up when loading metacpan.org $ENV{FIREFOX_NO_NETWORK} = 1; } } } } 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"); } $update = Firefox::Marionette::UpdateStatus->new(elevation_failure => 0, unsupported => undef, is_complete_update => 1, install_date => undef); ok(ref $update eq 'Firefox::Marionette::UpdateStatus', "Firefox::Marionette::UpdateStatus->new() produces a Firefox::Marionette::UpdateStatus object"); ok($update->elevation_failure() == 0, "\$update->elevation_failure() == 0 when parameter is 0"); ok(!defined $update->unsupported(), "\$update->unsupported() is not defined when parameter is not defined"); ok($update->is_complete_update() == 1, "\$update->is_complete_update() == 1 when parameter is 1"); ok(!defined $update->install_date(), "\$update->install_date() is not defined when parameter is not defined"); } 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 = 23; 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"); if ($major_version >= $min_stealth_version) { TODO: { local $TODO = "Some installations of firefox can default to webdriver being off"; # such as http://www.cpantesters.org/cpan/report/a0532bce-c32c-11ee-ae2f-883f6e8775ea (FreeBSD 14.0-STABLE) (BuildID 20240123011445) my $webdriver = $firefox->script('return navigator.webdriver'); ok($webdriver, "navigator.webdriver returns true:" . (defined $webdriver ? $webdriver : q[undef])); } } 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("about:blank", "_blank");')), "Opening new window to about:blank 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); my $metacpan_bookmark_icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGbElEQVRYhcWWTWwV1xXHf+fcO8/PNjYFTFH5MIJNJJQIDE0LwU4faRaojUQlJHaITbtglVUWjbpwF0gsk32zqCxFFUiVsijpJvIjDkppagG1RMTG1FZJRWMixd+emXtPF2/med7DJptKudLTzJt77v/8z/eFypoEzwvWd+0D3AD3YoxGB4ZUwc9B/tHQSwP7BnsvR3MNLO5B9JlKaD5dXJu4sPBoqZTbTvklCAB3jo5cJMpbIhwSs1Ww6TyGibH5mdlx0HEwwKSq/G+HT/zC0D8OJMlQIBIBBRzKUpYtCPHK6bn7t7YiUSqfGn7lqPf+Zr/6kwCx2PcoyzEH4m9Pz967biACJlXlPUnylxCNzGIKpkKLJkhMRGtOhY0s+2U3icKi+MnB4wf6vP6zz/vdqyGk0jJQSgyBZKevyWK+fu2nj+//7gY4Afho6KWBH/b3zyZOh3ILGaIJZoaIbD5j5sUlWYgL/11ZOXph4dFSaYWBCsTPj5z8eIdLzq+EbB2ROma2GWwRzKJhse68X0+z0TPz9+8owL7B3ssDSTKUWUy3Vm6GaJJZTAeSZGjfYO9lgCYNVyqfOjBy3IueXw5pAHraykWkgqWAOUCde5sixERzjUAETNsHS+XlMjMwDUSiuUb5uUlDAZy30bo6ZDPstM9XMAVxaTQC4Sc3OFbT1kbcE6slUbLuApIyqSzuAWjQjJsiDLHd6sKLGGIy8KNh198iIPpM2wlXYdz130qXiT6rWt8SYaFq/HPnK5iKYGJL/5kPKwqgEpoOBSR2uK07iZDoUFRCs/xceiHk8tl6NKzg2GFIiSkihoWaCuD+fomHqQI8XVybWMqyhUS0hsWseqBaBYlobSnLFp4urk0UyoNANNCxJ/ce5Bb/usPVHLDxnCFFFQASAAnhfQCdBH9h4dGSEK84Fby4xMxSwwIWc8OCmaVeXOJUEOKVsiNKEbXfFwavZuHXy3n6Ta9zdTNLwXKwUGIA8gPf49OQXTszf/9Ouw98r52wBPi+ZkEHwP9jGnYk4ZYY20zDCoA0aThoZXhZakXCWbf8dyn6mqYB7KUht2nG8a5afY5AaUW1xhs0o2xb5FuvcdCfVTC+pmlleF54aDsXWituL3RvISfdbq6u7guLVDdKhlMHRo4n3kZNGDJjIeTy2diTew+65bYyoHTx7f0vH6p5fUPUHY6iqzEyPTo3PQmt8JyjmbcJlKCfHDx+oC9xf/Ci5+u6SXQ9BjLix2tp+M3P//3gyVYkSuUGcvfIyHvA1T7nEymclhNZy/OHJvnVs49nPrVWDwjSUb/qv9jha7uXQxqqMTfQHa7mlvP0mzzmr47Nz8yWY7h0e3EvkLvDJ5qDPfXXF0MGFrNqaOrOeYCVLLs4Ovfgz5M0vJaWeO9v9nm/eyVk64IoogmIRzQRRFdCtt7n/W7v/c3CdW2CZdXcPTLy3mBP/fVv83TNijtEG0PUr4eQZdHwqn+a2n9q+BzNXKHVNPrVn1wNIUWk3qJcuYwAiNRXQ0j71Z+8c3TkYhnLcdBzNPPb+18+BFxdDBlAXaoYxbuIJsFsY4erJT6J70CZ1VHealllm8OnOoiKdyk7ZyEPUJZazesbfc4nWMykOv+rU7V1qfEbFkD0zTYBNQ4W/pQuqzdBWu8SC3nYbDIAou5wmXAdE7D6KxQEMyDunRoe3lVcSGytSBQ62XY+N7W15Ksriq52fKjeqKo4LRKYkPbmezdKytO++0Ky5VNiS86modVeSy/EyHROxLq92EXIsOhVwPjyx19NrypAHsPEcshMIMEstgG6LhMCyXLILI9hAuA2zVhW0ejc9ORanj+sO+etvNRUMdphlJCgYPoBgE7S8GPzM7OIvbvT1wQsYjGzYlxai3QGFnf6miD27tj8zGxRARE226tJfhXAiSZitmFYpI1jwcw2dvqkZylPb702N/2hgeg5mvk46OnZe9cX8/VrPc77PueTRFRVRBNR7XM+6XHeL+br107P3rtell7p3UsQJmn4s49nPl3JsovRYtbvkp6aOqci6kS17rwfcEnPt1l6K/mX/mozH8q0av2xz4dPnFXn3g7EV8UYNGER9AsJ4f0z8/fvFF0PthjNZXud2n9q2CfxHUTfFOLeKKQYX2L6wWtz0x9C59xor+qkusGx2tTwK7tucKy21f52q3sSTg0P7/rH/lN9FZJilSH4Pylr2D7oCD10AAAAAElFTkSuQmCC'; my $metacpan_bookmark_icon_url = 'fake-favicon-uri:https://metacpan.org/pod/Firefox::Marionette'; my $metacpan_uri = 'https://metacpan.org/'; SKIP: { if (($ENV{FIREFOX_NO_RECONNECT})) { if ($ENV{FIREFOX_HOST}) { skip("$ENV{FIREFOX_HOST} is not supported for reconnecting yet", 8); } else { 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 => 'timestamp,cookie:2', 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(process_alive($firefox_pid), "Can contact firefox process ($firefox_pid)"); } $firefox = undef; if (!$ENV{FIREFOX_HOST}) { ok(process_alive($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(!process_alive($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); } if (($ENV{WATERFOX}) || ($ENV{WATERFOX_VIA_FIREFOX})) { skip("No profile testing when any WATERFOX override is used", 6); } if ($ENV{FIREFOX_DEVELOPER}) { skip("No profile testing when the FIREFOX_DEVELOPER override is used", 6); } if ($ENV{FIREFOX_NIGHTLY}) { skip("No profile testing when the FIREFOX_NIGHTLY override is used", 6); } my $name = '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 has started in Marionette mode with a profile_name"); 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 when using a profile_name"); 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"); } else { 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); } if (($ENV{WATERFOX}) || ($ENV{WATERFOX_VIA_FIREFOX})) { skip("No profile testing when any WATERFOX override is used", 6); } if ($ENV{FIREFOX_DEVELOPER}) { skip("No profile testing when the FIREFOX_DEVELOPER override is used", 6); } if ($ENV{FIREFOX_NIGHTLY}) { skip("No profile testing when the FIREFOX_NIGHTLY override is used", 6); } my $found; my @names = Firefox::Marionette::Profile->names(); foreach my $name (@names) { if ($name eq 'throw') { $found = 1; } } if (!$found) { skip("No profile testing when throw profile doesn't exist", 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 when using a profile_name"); ok(process_alive($firefox_pid), "Can contact firefox process ($firefox_pid) when using a profile_name"); $firefox = undef; ok(process_alive($firefox_pid), "Can contact firefox process ($firefox_pid) when using a profile_name"); ($skip_message, $firefox) = start_firefox(0, debug => 1, reconnect => 1, profile_name => $name); ok($firefox, "Firefox has reconnected in Marionette mode when using a profile_name"); ok($firefox_pid == $capabilities->moz_process_id(), "Firefox has the same process id when using a profile_name"); $firefox = undef; ok(!process_alive($firefox_pid), "Cannot contact firefox process ($firefox_pid)"); } } if ($^O eq 'MSWin32') { } 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:$!"); $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', '-new', '-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 $proxyPort = URI->new($daemon->url())->port(); my $securePort = empty_port(); diag("Using proxy port TCP/$proxyPort"); my $socksPort = empty_port(); diag("Using SOCKS port TCP/$socksPort"); my %proxy_parameters = (http => 'localhost:' . $proxyPort, https => 'localhost:' . $securePort, none => [ 'local.example.org' ], socks => 'localhost:' . $socksPort); my $ftpPort = empty_port(); if ($binary =~ /waterfox/i) { } elsif ((defined $major_version) && ($major_version < 90)) { diag("Using FTP port TCP/$ftpPort"); $proxy_parameters{ftp} = 'localhost:' . $ftpPort; } my $proxy = Firefox::Marionette::Proxy->new(%proxy_parameters); my $bookmarks_path = File::Spec->catfile(Cwd::cwd(), qw(t data bookmarks_edge.html)); if ($major_version == 38) { skip("Skipping b/c of segmentation faults for proxy capabilities", 6); } ($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)), bookmarks => $bookmarks_path); 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"); if ($major_version < 61) { skip("HAR support not available in Firefox before version 61", 1); } else { my $har_lives = 0; eval { $firefox->har(); $har_lives = 1 }; chomp $@; ok($har_lives == 0 && $@ =~ /^(?:webdriver|javascript|unknown)[ ]error:[ ]TypeError:[ ](?:can't[ ]access[ ]property[ ]"triggerExport",[ ])?window[.]HAR[ ]is[ ]undefined[ ]at[ ]t[\/\\]01\-marionette.t[ ]line[ ]\d+/smx, "\$firefox->har() throws an exception when har has not been setup:$@"); } if ($major_version >= 60) { my ($bookmark) = $firefox->bookmarks({ url => URI::URL->new($metacpan_uri . 'pod/Firefox::Marionette') }); ok($bookmark, "Retrieved bookmark from edge import"); ok(ref $bookmark->url() eq 'URI::URL', "\$bookmark->url() returns a URI::URL object"); ok($bookmark->url() eq $metacpan_uri . 'pod/Firefox::Marionette', "\$bookmark->url() is '${metacpan_uri}pod/Firefox::Marionette':" . $bookmark->url()); ok($bookmark->date_added() == 1685610972, "\$bookmark->date_added() is " . localtime $bookmark->date_added()); ok($bookmark->title() eq 'Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org', "\$bookmark->title() is 'Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org':" . $bookmark->title()); ok($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK(), "\$bookmark->type() is Firefox::Marionette::Bookmark::BOOKMARK():" . $bookmark->type()); ok($bookmark->parent_guid(), "\$bookmark->parent_guid() " . $bookmark->parent_guid()); ok($bookmark->guid(), "\$bookmark->guid() is " . $bookmark->guid()); ($bookmark) = $firefox->bookmarks({ url => URI::URL->new('https://perlmonks.org/') }); ok($bookmark->url() eq 'https://perlmonks.org/', "\$bookmark->url() is 'https://perlmonks.org/':" . $bookmark->url()); ok($bookmark->date_added() == 1686364081, "\$bookmark->date_added() is " . localtime $bookmark->date_added()); ok($bookmark->title() eq 'PerlMonks - The Monastery Gates', "\$bookmark->title() is 'PerlMonks - The Monastery Gates':" . $bookmark->title()); ok($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK(), "\$bookmark->type() is Firefox::Marionette::Bookmark::BOOKMARK():" . $bookmark->type()); ok($bookmark->parent_guid(), "\$bookmark->parent_guid() is " . $bookmark->parent_guid()); ok($bookmark->guid(), "\$bookmark->guid() is " . $bookmark->guid()); ok(!defined $bookmark->icon_url(), "\$bookmark->icon_url() is not defined"); ok(!defined $bookmark->icon(), "\$bookmark->icon() is not defined"); } 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()); my $moz_use_non_spec_compliant_pointer_origin = $capabilities->moz_use_non_spec_compliant_pointer_origin(); if (defined $moz_use_non_spec_compliant_pointer_origin) { ok($moz_use_non_spec_compliant_pointer_origin == 0, "\$capabilities->moz_use_non_spec_compliant_pointer_origin() is set to false"); } else { ok(1, "\$capabilities->moz_use_non_spec_compliant_pointer_origin() is not defined"); } } else { 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:' . $proxyPort, "\$capabilities->proxy()->http() is 'localhost:" . $proxyPort . "':" . $capabilities->proxy()->http()); ok($capabilities->proxy()->https() eq 'localhost:' . $securePort, "\$capabilities->proxy()->https() is 'localhost:" . $securePort . "'"); if ($major_version < 90) { ok($capabilities->proxy()->ftp() eq 'localhost:' . $ftpPort, "\$capabilities->proxy()->ftp() is 'localhost:$ftpPort'"); } 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 'localhost:' . $socksPort, "\$capabilities->proxy()->socks() is 'localhost:$socksPort':" . $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 ($^O eq 'cygwin') { skip("\$capabilities->proxy is not supported for $^O", 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'; wait_for_server_on($daemon, $daemon->url(), $pid); $daemon = undef; my $try_count = 0; GO: { eval { $firefox->go($url); } or do { if ($try_count < 2) { diag("Failed to get $url via proxy on attempt $try_count for $version_string"); $try_count += 1; redo GO; } else { diag("Failed to get $url via proxy too many times $version_string"); } }; } ok($firefox->html() =~ /success/smx, "Correctly accessed the Proxy"); diag($firefox->html()); $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"; } } while(kill 0, $pid) { kill $signals_by_name{TERM}, $pid; sleep 1; waitpid $pid, POSIX::WNOHANG(); } ok($! == POSIX::ESRCH(), "Process $pid no longer exists:$!"); } elsif (defined $pid) { eval 'Devel::Cover::set_coverage("none")' if $is_covering; eval { local $SIG{ALRM} = sub { die "alarm during proxy server\n" }; alarm 40; $0 = "[Test HTTP Proxy for " . getppid . "]"; diag("Accepting connections for $0"); 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 40; 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"); } } TODO: { local $TODO = $correct_exit_status == 0 ? q[] : "$version_string is not exiting cleanly"; ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } } SKIP: { my $proxyPort = empty_port(); my $latitude = 40; my $longitude = 24; my $geo1 = Firefox::Marionette::GeoLocation->new(lat => $latitude, lng => $longitude); diag("Starting new firefox for testing proxies with proxy port TCP/$proxyPort"); if (($major_version == 45) || ($major_version == 38)) { skip("Skipping b/c of segmentation faults for proxy capabilities", 6); } ($skip_message, $firefox) = start_firefox(0, chatty => 1, devtools => 1, page_load => 65432, capabilities => Firefox::Marionette::Capabilities->new(proxy => Firefox::Marionette::Proxy->new( pac => URI->new('http://localhost:' . $proxyPort)), moz_headless => 1), geo => $geo1); 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 'localhost', "\$capabilities->proxy()->pac()->host() is 'localhost'"); } ok($capabilities->timeouts()->page_load() == 65432, "\$firefox->capabilities()->timeouts()->page_load() correctly reflects the page_load shortcut timeout"); if ($ENV{FIREFOX_HOST}) { } elsif (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { diag("Skipping checks that use a file:// url b/c of OpenBSD's unveil functionality - see https://bugzilla.mozilla.org/show_bug.cgi?id=1580271"); } elsif ($major_version >= $min_geo_version) { 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"); my $geo2; eval { $geo2 = $firefox->geo(); }; if ((!defined $geo2) && (($uname eq 'cygwin') || ($uname eq 'MSWin32'))) { diag("Location services may be disabled:$@"); eval { $firefox->dismiss_alert(); }; } elsif (defined $geo2) { my $returned_latitude = $geo2->latitude(); my $returned_longitude = $geo2->longitude(); ok($returned_latitude == $latitude, "\$geo2->latitude() is correctly reported as $latitude:$returned_latitude"); ok($returned_longitude == $longitude, "\$geo2->longitude() is correctly reported as $longitude:$returned_longitude"); } } TODO: { local $TODO = $correct_exit_status == 0 ? q[] : "$version_string is not exiting cleanly"; ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } } SKIP: { my $proxyPort = Firefox::Marionette::Proxy::DEFAULT_SQUID_PORT(); diag("Starting new firefox for testing proxies again using default proxy port TCP/$proxyPort"); my $visible = 1; if (($ENV{FIREFOX_HOST}) && ($ENV{FIREFOX_HOST} eq 'localhost') && ($ENV{FIREFOX_USER})) { $visible = 'local'; } if ($major_version == 38) { skip("Skipping b/c proxy must be undefined", 7); } if ($major_version >= 135) { skip("Skipping b/c proxy seems to cause hangs", 7); } ($skip_message, $firefox) = start_firefox($visible, seer => 1, chatty => 1, capabilities => Firefox::Marionette::Capabilities->new(proxy => Firefox::Marionette::Proxy->new( host => 'localhost', none => 'localhost'))); 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 'localhost:' . $proxyPort, "\$capabilities->proxy()->https() is 'localhost:$proxyPort'"); ok($capabilities->proxy()->http() eq 'localhost:' . $proxyPort, "\$capabilities->proxy()->http() is 'localhost:$proxyPort'"); local $TODO = $major_version < 58 ? $capabilities->browser_version() . " does not have support for \$firefox->capabilities()->none()" : q[]; my $none_count = 0; foreach my $host ($capabilities->proxy()->none()) { ok($host eq 'localhost', "\$capabilities->proxy()->none() is 'localhost'"); $none_count += 1; } ok($none_count == 1, "Number of none proxies is 1:$none_count"); } if (($ENV{RELEASE_TESTING}) && ($visible eq 'local')) { `xwininfo -version 2>/dev/null`; if ($? == 0) { require Crypt::URandom; my $string = join q[], unpack("h*", Crypt::URandom::urandom(20)); $firefox->script('window.document.title = arguments[0]', args => [ $string ]); my $found_window = `xwininfo -root -tree | grep $string`; chomp $found_window; ok($found_window, "Found X11 Forwarded window:$found_window"); my $pid = $capabilities->moz_process_id(); if (defined $pid) { my $command = "ps axo pid,user,cmd | grep -E '^[ ]+$pid\[ \]+$ENV{FIREFOX_USER}\[ \]+.+firefox[ ]\-marionette[ ]\-safe\-mode[ ]\-profile[ ].*/profile[ ]\-\-no\-remote[ ]\-\-new\-instance[ ]*\$'"; my $process_listing = `$command`; chomp $process_listing; ok($process_listing =~ /^[ ]+$pid/, "Found X11 Forwarded process:$process_listing"); } } } my $child_error = $firefox->quit(); if (($major_version < 50) && ($ENV{RELEASE_TESTING}) && ($visible eq 'local')) { $correct_exit_status = $child_error; } TODO: { local $TODO = $correct_exit_status == 0 ? q[] : "$version_string is not exiting cleanly"; 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"); my $bookmarks_path = File::Spec->catfile(Cwd::cwd(), qw(t data bookmarks_chrome.html)); ($skip_message, $firefox) = start_firefox(0, capabilities => Firefox::Marionette::Capabilities->new(accept_insecure_certs => 1, moz_headless => 1), bookmarks => $bookmarks_path, geo => 1, stealth => 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"); if ($major_version < 30) { diag("Skipping WebGL as it can cause older browsers to hang"); } elsif ($firefox->script(q[let c = document.createElement('canvas'); return c.getContext('webgl2') ? true : c.getContext('experimental-webgl') ? true : false;])) { diag("WebGL is enabled by default when visible and addons are turned off"); } else { diag("WebGL is disabled by default when visible and addons are turned off"); } if ($major_version >= 60) { my ($bookmark) = $firefox->bookmarks({ url => URI::URL->new($metacpan_uri . 'pod/Firefox::Marionette') }); ok($bookmark, "Retrieved bookmark from chrome import as " . $bookmark->url()); ok($bookmark->url() eq $metacpan_uri . 'pod/Firefox::Marionette', "\$bookmark->url() is '${metacpan_uri}pod/Firefox::Marionette':" . $bookmark->url()); ok($bookmark->date_added() == 1685610972, "\$bookmark->date_added() is " . localtime $bookmark->date_added()); ok($bookmark->title() eq 'Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org', "\$bookmark->title() is 'Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org':" . $bookmark->title()); ok($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK(), "\$bookmark->type() is Firefox::Marionette::Bookmark::BOOKMARK():" . $bookmark->type()); ok($bookmark->parent_guid(), "\$bookmark->parent_guid() is " . $bookmark->parent_guid()); ok($bookmark->guid(), "\$bookmark->guid() is " . $bookmark->guid()); ($bookmark) = $firefox->bookmarks({ url => URI::URL->new('https://perlmonks.org/') }); ok($bookmark->url() eq 'https://perlmonks.org/', "\$bookmark->url() is 'https://perlmonks.org/':" . $bookmark->url()); ok($bookmark->date_added() == 1686364081, "\$bookmark->date_added() is " . localtime $bookmark->date_added()); ok($bookmark->title() eq 'PerlMonks - The Monastery Gates', "\$bookmark->title() is 'PerlMonks - The Monastery Gates':" . $bookmark->title()); ok($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK(), "\$bookmark->type() is Firefox::Marionette::Bookmark::BOOKMARK():" . $bookmark->type()); ok($bookmark->parent_guid(), "\$bookmark->parent_guid() is " . $bookmark->parent_guid()); ok($bookmark->guid(), "\$bookmark->guid() is " . $bookmark->guid()); ok(!defined $bookmark->icon_url(), "\$bookmark->icon_url() is not defined"); ok(!defined $bookmark->icon(), "\$bookmark->icon() is not defined"); } if ($ENV{FIREFOX_HOST}) { } elsif (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { diag("Skipping checks that use a file:// url b/c of OpenBSD's unveil functionality - see https://bugzilla.mozilla.org/show_bug.cgi?id=1580271"); } elsif ($arch =~ /$arch_32bit_re/smx) { diag("aria tests can cause hangs in 32 bit architectures. See debian failures such as https://tests.reproducible-builds.org/debian/rbuild/trixie/armhf/libfirefox-marionette-perl_1.53-1.rbuild.log.gz"); } elsif ($major_version >= 113) { # https://bugzilla.mozilla.org/show_bug.cgi?id=1585622 my $path = File::Spec->catfile(Cwd::cwd(), qw(t data aria.html)); if ($^O eq 'cygwin') { $path = $firefox->execute( 'cygpath', '-s', '-m', $path ); } $firefox->go("file://$path"); my $element = $firefox->find_id('close'); ok($firefox->aria_label($element) eq 'Close', "Retrieved the ARIA label correctly:" . $firefox->aria_label($element)); ok($firefox->find_id('close')->aria_label() eq 'Close', "Retrieved the ARIA label correctly:" . $firefox->find_id('close')->aria_label()); $element = $firefox->find_id('save'); ok($firefox->aria_role($element) =~ /^(?:toggle[ ])?button$/smx, "Retrieved the ARIA role correctly:" . $firefox->aria_role($element)); ok($firefox->find_id('save')->aria_role() =~ /^(?:toggle[ ])?button$/smx, "Retrieved the ARIA label correctly:" . $firefox->find_id('save')->aria_role()); } if ($major_version > 32) { # https://bugzilla.mozilla.org/show_bug.cgi?id=889335 my $browser_language = join q[, ], @{$firefox->script('return navigator.languages')}; my $original_language = join q[, ], $firefox->languages(); ok($original_language eq $browser_language, "\$firefox->languages() equals navigator.languages:'$original_language' vs '$browser_language'"); my $new_language = 'en-AU, en-GB, en'; ok((join q[, ], $firefox->languages(split q[, ], $new_language)) eq $original_language, "\$firefox->languages(split q[, ], \"$new_language\") returns correctly"); $browser_language = join q[, ], @{$firefox->script('return navigator.languages')}; ok($new_language eq $browser_language, "\$firefox->languages() equals navigator.languages:'$new_language' vs '$browser_language'"); my $lone_language = 'en-GB'; ok((join q[, ], $firefox->languages($lone_language)) eq $new_language, "\$firefox->languages(\"$lone_language\") returns correctly"); $browser_language = join q[, ], @{$firefox->script('return navigator.languages')}; ok($lone_language eq $browser_language, "\$firefox->languages() matches navigator.language b/c there is only one entry:'$lone_language' vs '$browser_language'"); } else { my $browser_language = $firefox->chrome()->script('return Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService).getBranch("").getComplexValue("intl.accept_languages", Components.interfaces.nsIPrefLocalizedString).data;'); $firefox->content(); my $original_language = join q[, ], $firefox->languages(); ok($original_language eq $browser_language, "\$firefox->languages() equals navigator.languages:'$original_language' vs '$browser_language'"); } my $test_agent_string = "Firefox::Marionette v$Firefox::Marionette::VERSION test suite"; ok($firefox->agent($test_agent_string) eq $original_agent, "\$firefox->agent(\$test_agent_string) returns the original user agent string of '$original_agent'"); my $shadow_root; my $path; if ($ENV{FIREFOX_HOST}) { } elsif (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { diag("Skipping checks that use a file:// url b/c of OpenBSD's unveil functionality - see https://bugzilla.mozilla.org/show_bug.cgi?id=1580271"); } else { $path = File::Spec->catfile(Cwd::cwd(), qw(t data elements.html)); if ($^O eq 'cygwin') { $path = $firefox->execute( 'cygpath', '-s', '-m', $path ); } ok($firefox->go("file://$path"), "\$firefox->go(\"file://$path\") loaded successfully"); my $new_agent = $firefox->agent(undef); ok($new_agent eq $test_agent_string, "\$firefox->agent(undef) returns '$test_agent_string':$new_agent"); ok($firefox->go("file://$path"), "\$firefox->go(\"file://$path\") loaded successfully again"); my $final_agent = $firefox->agent(); ok($final_agent eq $original_agent, "\$firefox->agent() returns the original user agent string '$original_agent':$final_agent"); $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"); } elsif ($count == 1) { ok($element->tag_name() eq 'div', "Second element from ShadowRoot via script is a div tag"); } $count += 1; } ok($count == 6, "\$firefox->has_tag('custom-square')->shadow_root() has 2 children:$count"); 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"); if ($major_version >= 113) { ok($firefox->find_id('outer-div', $shadow_root)->attribute('title') eq 'In the Shadow Realms', "Correctly found shadow element with find_id"); my $shadow_count = 0; foreach my $span ($firefox->has_tag('span', $shadow_root)) { ok($span->tag_name() eq 'span', "Correctly found shadow span with find_tag"); $shadow_count += 1; } ok($shadow_count == 2, "There are 2 span elements in the custom-square element"); $shadow_count = 0; foreach my $span ($firefox->has_tag('notag', $shadow_root)) { $shadow_count += 1; } ok($shadow_count == 0, "There are 0 notag elements in the custom-square element"); ok($firefox->find_name('meta-name', $shadow_root)->attribute('title') eq 'Very META', "Correctly found shadow element with find_name"); ok($firefox->find_class('outer-div-class', $shadow_root)->attribute('title') eq 'In the Shadow Realms', "Correctly found shadow element with find_class"); ok($firefox->find_link('MetaCPAN', $shadow_root)->attribute('href') eq 'https://metacpan.org', "Correctly found shadow element with find_link"); ok($firefox->find_partial('Meta', $shadow_root)->attribute('href') eq 'https://metacpan.org', "Correctly found shadow element with find_partial"); TODO: { local $TODO = "xpath is not supported as a strategy for find in the shadow DOM"; my $title = q[]; eval { $title = $firefox->find('//div', $shadow_root)->attribute('title'); }; ok($title eq 'In the Shadow Realms', "Correctly found shadow element with find (xpath):$title"); } } } { 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::Marionette::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::Marionette::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 $webdriver = $firefox->script('return navigator.webdriver'); ok(!$webdriver, "navigator.webdriver returns false when stealth is on"); if (($tls_tests_ok) && ($ENV{RELEASE_TESTING})) { $firefox->chrome(); foreach my $name (@Firefox::Marionette::DNS::EXPORT_OK) { my $correct = $firefox->script("return Components.interfaces.nsIDNSService.$name"); my $actual = eval "return Firefox::Marionette::DNS::$name();"; local $TODO = ($major_version < 115 && $name =~ /^((?:RESOLVE_(?:TYPE_DEFAULT|TYPE_TXT|TYPE_HTTPSSVC|ALLOW_NAME_COLLISION|DISABLE_TRR|REFRESH_CACHE|TRR_MODE_MASK|TRR_DISABLED_MODE|IGNORE_SOCKS_DNS|IP_HINT|WANT_RECORD_ON_ERROR))|ALL_DNSFLAGS_BITS)$/smx) ? "Older firefox (less than 115) can have different values for Firefox::Marionette::DNS constants" : q[]; local $TODO = $TODO || (($major_version < 130 && $name =~ /^((?:RESOLVE_(?:CREATE_MOCK_HTTPS_RR|DISABLE_NATIVE_HTTPS_QUERY))|ALL_DNSFLAGS_BITS)$/smx) ? "Older firefox (less than 130) can have different values for Firefox::Marionette::DNS constants" : q[]); ok(defined $correct && defined $actual && $correct == $actual, "Firefox::Marionette::DNS::$name() ($actual) matches the value in firefox (" . (defined $correct ? $correct : "null") . ")"); } $firefox->content(); if ($major_version >= 52) { foreach my $result ($firefox->resolve('localhost')) { ok($result =~ /^(127[.]0[.]0[.]1|::1)/smx, "\$firefox->resolve('localhost') returned correctly:$result"); } foreach my $result ($firefox->resolve('localhost', type => 0, flags => 0)) { ok($result =~ /^(127[.]0[.]0[.]1|::1)/smx, "\$firefox->resolve('localhost', type => 0, flags => 0) returned correctly:$result"); } if ($major_version >= 78) { my $test_dns_name = 'custom-weird.example.com'; my $ip_address = '127.0.0.84'; foreach my $result ($firefox->resolve_override($test_dns_name, $ip_address)->resolve($test_dns_name)) { ok($result eq $ip_address, "\$firefox->resolve_override('$test_dns_name', '$ip_address') worked correctly:$result"); } } } my $json; if ($major_version < 50) { diag("\$firefox->json(\$url) calls aren't going to work for versions < 50"); } else { $json = $firefox->json($freeipapi_uri); ok($json->{ipVersion} == 6, "\$firefox->json(\$url)->{ipVersion} returned 6:$json->{ipVersion}"); ok($json->{ipAddress} eq '2001:8001:4ab3:d800:7215:c1fe:fc85:1329', "\$firefox->json(\$url)->{ipAddress} returned '2001:8001:4ab3:d800:7215:c1fe:fc85:1329':$json->{ipAddress}"); ok($json->{latitude} == -37.5, "\$firefox->json(\$url)->{latitude} returned -31.5:$json->{latitude}"); ok($json->{longitude} == 144.5, "\$firefox->json(\$url)->{longitude} returned 144.5:$json->{longitude}"); ok($json->{timeZone} eq "+11:00", "\$firefox->json(\$url)->{timeZone} returned +11:00:$json->{timeZone}"); $useragents_me_uri =~ s/[ ]/%20/smxg; # firefoxen older that 108 strips spaces from data uris: https://bugzilla.mozilla.org/show_bug.cgi?id=1104311 my %user_agent_strings = map { $_->{ua} => $_->{pct} } @{$firefox->json($useragents_me_uri)->{data}}; my ($user_agent) = reverse sort { $user_agent_strings{$a} <=> $user_agent_strings{$b} } keys %user_agent_strings; ok($user_agent eq $most_common_useragent, "Correctly sorted the most common user agent:'$user_agent' vs '$most_common_useragent'"); ok($firefox->agent($most_common_useragent), "\$firefox->agent(\"\$most_common_useragent\") worked"); if ($ENV{FIREFOX_HOST}) { } elsif (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { diag("Skipping checks that use a file:// url b/c of OpenBSD's unveil functionality - see https://bugzilla.mozilla.org/show_bug.cgi?id=1580271"); } else { ok($firefox->go("file://$path"), "\$firefox->go(\"file://$path\") loaded successfully for user agent test"); my $agent = $firefox->agent(); ok($agent eq $most_common_useragent, "\$firefox->agent() now produces the most common user agent"); } } if ($ENV{FIREFOX_HOST}) { } elsif (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { } elsif ($major_version >= $min_geo_version) { my %hosts = ($freeipapi_uri => 'freeipapi', $geocode_maps_uri => 'geocode_maps', $positionstack_uri => 'positionstack', $ipgeolocation_uri => 'ipgeolocation', $ipstack_uri => 'ipstack', $dummy1_uri => 'dummy1', $dummy2_uri => 'dummy2'); foreach my $geo_uri ($freeipapi_uri, $geocode_maps_uri, $positionstack_uri, $ipgeolocation_uri, $ipstack_uri, $dummy1_uri, $dummy2_uri) { my $json = $firefox->json($geo_uri); if ($geo_uri eq $geocode_maps_uri) { $json = $json->[0]; } elsif ($geo_uri eq $positionstack_uri) { $json = $json->{data}->[0]; } my $sample = Firefox::Marionette::GeoLocation->new($json); my $latitude = $sample->latitude(); my $longitude = $sample->longitude(); if (($geo_uri eq $dummy1_uri) || ($geo_uri eq $dummy2_uri)) { ok($latitude == 40.7, "Firefox::Marionette::GeoLocation latitude from $hosts{$geo_uri} is 40.7:" . $latitude); ok($longitude == -73.9, "Firefox::Marionette::GeoLocation longitude from $hosts{$geo_uri} is -73.9:" . $longitude); } else { ok($latitude == -37.5, "Firefox::Marionette::GeoLocation latitude from $hosts{$geo_uri} is -37.5:" . $latitude); ok($longitude == 144.5, "Firefox::Marionette::GeoLocation longitude from $hosts{$geo_uri} is 144.5:" . $longitude); } my $timezone_offset = $sample->timezone_offset(); if (($geo_uri eq $geocode_maps_uri) || ($geo_uri eq $positionstack_uri) || ($geo_uri eq $ipstack_uri) || ($geo_uri eq $dummy2_uri)) { ok(!defined $timezone_offset, "Firefox::Marionette::GeoLocation timezone offset from $hosts{$geo_uri} is not defined"); } elsif ($geo_uri eq $dummy1_uri) { ok($timezone_offset == 300, "Firefox::Marionette::GeoLocation timezone offset from $hosts{$geo_uri} is 300:$timezone_offset"); } else { ok(defined $timezone_offset, "Firefox::Marionette::GeoLocation timezone offset from $hosts{$geo_uri} is defined:$timezone_offset"); } my $country_code = $sample->country_code(); if (($geo_uri eq $geocode_maps_uri) || ($geo_uri eq $positionstack_uri) || ($geo_uri eq $dummy1_uri) || ($geo_uri eq $dummy2_uri)) { ok(!defined $country_code, "Firefox::Marionette::GeoLocation country_code from $hosts{$geo_uri} is not defined"); } else { ok($country_code eq 'AU', "Firefox::Marionette::GeoLocation country_code from $hosts{$geo_uri} is 'AU':$country_code"); } } } my $new_york = Firefox::Marionette::GeoLocation->new({ lat => 40.753, long => -73.983 }); my $encoded_new_york = Encode::encode('UTF-8', "$new_york", 1); ok($encoded_new_york eq "40°45'11\"N,73°58'59\"W", "Correctly stringifies New York:$encoded_new_york"); ok($new_york->uri()->isa('URI') && $new_york->uri() eq 'geo:40.753,-73.983', "\$geo->uri() correctly produces a URI:" . $new_york->uri()); my $fountain_island = Firefox::Marionette::GeoLocation->new(latitude => -37.82896, longitude => 144.9811, accuracy => 40075017, altitude => 55, altitude_accuracy => 20, speed => 20); my $encoded_fountain_island = Encode::encode('UTF-8', "$fountain_island", 1); ok($encoded_fountain_island eq "37°49'44\"S,144°58'52\"E", "Correctly stringifies Fountain Island:$encoded_fountain_island"); ok($fountain_island->uri()->isa('URI') && $fountain_island->uri() eq 'geo:-37.82896,144.9811,55;u=40075017', "\$geo->uri() correctly produces a URI:" . $fountain_island->uri()); my ($latitude, $longitude); if ($ENV{FIREFOX_HOST}) { } elsif (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { } elsif ($major_version >= $min_geo_version) { $firefox->geo($json); if (my $geo3 = $firefox->geo()) { $latitude = $geo3->latitude(); ok($latitude >= -90 && $latitude <= 90, "\$geo3->latitude() looks like a latitude >= -90 and <= 90:$latitude"); $longitude = $geo3->longitude(); ok($longitude >= -180 && $longitude <= 180, "\$geo3->longitude() looks like a longitude >= -180 and <= 180:$longitude"); my $timezone_offset = $geo3->timezone_offset(); ok(defined $timezone_offset, "\$geo3->timezone_offset() is the javascript timezone offset:$timezone_offset"); my $accuracy = $geo3->accuracy(); TODO: { local $TODO = ($major_version < 63) ? "\$geo3->accuracy() not available for older versions of firefox" : ($^O eq 'dragonfly') ? "\$geo3->accuracy can fail on DragonFly" : q[]; ok(defined $accuracy && $accuracy >= 0, "\$geo3->accuracy() is a positive float (accuracy in metres):" . (defined $accuracy ? $accuracy : q[])); } my $altitude = $geo3->altitude(); if (defined $altitude) { ok($altitude >= 0, "\$geo3->altitude() is a positive float (altitude in metres):$altitude"); } else { ok(1, "\$geo3->altitude() is not defined"); } my $altitude_accuracy = $geo3->altitude_accuracy(); if (defined $altitude_accuracy) { ok($altitude_accuracy >= 0, "\$geo3->altitude_accuracy() is a positive float (altitude accuracy in metres):$altitude_accuracy"); } else { ok(1, "\$geo3->altitude_accuracy() is not defined"); } my $heading = $geo3->heading(); if (defined $heading) { ok($heading >= 0 && $heading <= 360, "\$geo3->heading() looks like a heading >= 0 and <= 360:$latitude"); } else { ok(1, "\$geo3->heading() is not defined"); } my $speed = $geo3->speed(); if (defined $speed) { ok($speed >= 0, "\$geo3->speed() is a positive float (speed in metres per second):$speed"); } else { ok(1, "\$geo3->speed() is not defined"); } } elsif (($uname eq 'cygwin') || ($uname eq 'MSWin32')) { diag("Location services may be disabled"); eval { $firefox->dismiss_alert(); }; } } if ($major_version < 63) { diag("Not attempting to do cache operations for Firefox $major_version"); } else { if ($ENV{FIREFOX_HOST}) { } elsif (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { diag("Skipping checks that use a file:// url b/c of OpenBSD's unveil functionality - see https://bugzilla.mozilla.org/show_bug.cgi?id=1580271"); } elsif ($major_version >= $min_geo_version) { 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"); if (my $geo4 = $firefox->geo()) { ok($geo4->latitude() == $latitude, "\$geo4->latitude() has remained after a page load"); ok($geo4->longitude() == $longitude, "\$geo4->longitude() has remained after a page load"); } elsif (($uname eq 'cygwin') || ($uname eq 'MSWin32')) { diag("Location services may be disabled"); eval { $firefox->dismiss_alert(); }; } } ok($firefox->go('https://github.com/login'), "\$firefox->go('https://github.com/login') succeeded"); my $old_session_cookie = github_session_cookie($firefox); ok($old_session_cookie, "Found github session cookie"); ok($firefox->go('about:blank'), "\$firefox->go('about:blank') succeeded"); my $cookie_count = 0; foreach my $cookie ($firefox->cookies()) { $cookie_count += 1; diag("Should not have found cookie " . $cookie->name() . " for about:blank"); } ok($cookie_count == 0, "There are no availabe cookies for about:blank"); ok(ref $firefox->clear_cache() eq $class, "\$firefox->clear_cache() produces a $class object"); ok($firefox->go('https://github.com/login'), "\$firefox->go('https://github.com/login') succeeded"); my $new_session_cookie = github_session_cookie($firefox); ok(defined $new_session_cookie, "The session cookie was found after clearing cache"); ok($old_session_cookie ne $new_session_cookie, "Different session cookie found after clearing everything in the cache"); $old_session_cookie = $new_session_cookie; ok($firefox->go('about:blank'), "\$firefox->go('about:blank') succeeded"); ok(ref $firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_COOKIES()) eq $class, "\$firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_COOKIES()) produces a $class object"); ok($firefox->go('https://github.com/login'), "\$firefox->go('https://github.com/login') succeeded"); $new_session_cookie = github_session_cookie($firefox); ok(defined $new_session_cookie, "The session cookie was found after clearing cache"); ok($old_session_cookie ne $new_session_cookie, "Different session cookie found after clearing cookie cache"); $old_session_cookie = $new_session_cookie; ok($firefox->go('about:blank'), "\$firefox->go('about:blank') succeeded"); ok(ref $firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_NETWORK_CACHE()) eq $class, "\$firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_NETWORK_CACHE()) produces a $class object"); ok($firefox->go('https://github.com/login'), "\$firefox->go('https://github.com/login') succeeded"); $new_session_cookie = github_session_cookie($firefox); ok(defined $new_session_cookie, "The session cookie was found after clearing cache"); TODO: { local $TODO = q[Sometimes this doesn't work for github]; ok($old_session_cookie eq $new_session_cookie, "The same session cookie found after clearing network cache"); } } } Firefox::Marionette::Cache->import(qw(:all)); my $clear_data_service_is_ok = 1; eval { $firefox->check_cache_key('CLEAR_COOKIES'); } or do { $clear_data_service_is_ok = 0; chomp $@; diag("Unable to check cache values:$@"); }; if ($clear_data_service_is_ok) { foreach my $name ($firefox->cache_keys()) { no strict; TODO: { local $TODO = ($major_version < 113 && $name !~ /^(CLEAR_COOKIES|CLEAR_NETWORK_CACHE|CLEAR_IMAGE_CACHE)$/smx) ? "Older firefox (less than 113) can have different values for Firefox::Marionette::Cache constants" : q[]; local $TODO = $TODO || ($major_version < 128 && $name =~ /^(?:CLEAR_CREDENTIAL_MANAGER_STATE|CLEAR_COOKIE_BANNER_EXCEPTION|CLEAR_COOKIE_BANNER_EXECUTED_RECORD|CLEAR_FINGERPRINTING_PROTECTION_STATE|CLEAR_BOUNCE_TRACKING_PROTECTION_STATE|CLEAR_FORGET_ABOUT_SITE|CLEAR_STORAGE_PERMISSIONS|CLEAR_COOKIES_AND_SITE_DATA)$/) ? "Old firefox (less than 128) can have different values for Firefox::Marionette::Cache constants" : q[]; local $TODO = $TODO || ($major_version < 129 && $name =~ /^(?:CLEAR_PERMISSIONS|CLEAR_FORGET_ABOUT_SITE)$/) ? "Old firefox (less than 129) can have different values for Firefox::Marionette::Cache constants" : q[]; local $TODO = $TODO || ($major_version < 130 && $name =~ /^(?:CLEAR_ALL_CACHES|CLEAR_FORGET_ABOUT_SITE)$/) ? "Old firefox (less than 130) can have different values for Firefox::Marionette::Cache constants" : q[]; local $TODO = $TODO || ($major_version < 132 && $name =~ /^(?:CLEAR_SESSION_HISTORY|CLEAR_FORGET_ABOUT_SITE)$/) ? "Old firefox (less than 132) can have different values for Firefox::Marionette::Cache constants" : q[]; local $TODO = $TODO || ($major_version < 135 && $name =~ /^(?:CLEAR_COOKIES_AND_SITE_DATA|CLEAR_FORGET_ABOUT_SITE)$/) ? "Old firefox (less than 135) can have different values for Firefox::Marionette::Cache constants" : q[]; my $result = $firefox->check_cache_key($name); ok($result == &$name(), "\$firefox->check_cache_key($name) eq Firefox::Marionette::Cache::${name} which should be $result and is " . &$name()); } use strict; } } eval { $firefox->check_cache_key(); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->check_cache_key() throws an exception"); eval { $firefox->check_cache_key("123!#"); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->check_cache_key(\"123!#\") throws an exception"); ok($firefox->content(), "\$firefox->content() is called in case of previous exceptions getting the context out of sync"); 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"); if (!$ENV{RELEASE_TESTING}) { skip("Skipping network tests", 3); } ok($firefox->go('about:mozilla'), 'about:mozilla 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 }, scale => 1); $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 if ((centimetres_to_points(12) == $urx) || (centimetres_to_points(12) == $urx - 1)) { 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)"); } else { # at least like this since firefox 112 ok(((centimetres_to_points(12) == $ury) || (centimetres_to_points(12) == $ury - 1)) && ((centimetres_to_points(7) == $urx) || (centimetres_to_points(7) == $urx - 1)), "Correct page width of " . centimetres_to_points(7) . " (was actually $urx) and height " . centimetres_to_points(12) . " (was actually $ury)"); } 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 github_session_cookie { my ($firefox) = @_; my $session_name = '_gh_sess'; my $session_value; foreach my $cookie ($firefox->cookies()) { if ($cookie->name() eq $session_name) { $session_value = $cookie->value(); } } return $session_value; } 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"); my $bookmarks_path = File::Spec->catfile(Cwd::cwd(), qw(t data bookmarks_firefox.json)); ($skip_message, $firefox) = start_firefox(0, addons => 1, capabilities => Firefox::Marionette::Capabilities->new(moz_headless => 1), bookmarks => $bookmarks_path); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 4); } ok($firefox, "Firefox has started in Marionette mode with definable capabilities set to known values"); if ($major_version < 51) { diag("WebGL does not work and should not as version $major_version is older than 51"); } elsif ($firefox->script(q[let c = document.createElement('canvas'); return c.getContext('webgl2') ? true : c.getContext('experimental-webgl') ? true : false;])) { diag("WebGL appears to be enabled in headless mode (with addons => 1)"); } else { diag("WebGL appears to be disabled in headless mode (with addons => 1)"); } if ($major_version >= 60) { ok(ref $firefox->import_bookmarks($bookmarks_path) eq $class, "\$firefox->import_bookmarks('$bookmarks_path') returns itself for chaining"); my ($bookmark) = $firefox->bookmarks({ url => URI::URL->new($metacpan_uri . 'pod/Firefox::Marionette') }); ok($bookmark, "Retrieved bookmark from firefox export as " . $bookmark->url()); ok($bookmark->url() eq $metacpan_uri . 'pod/Firefox::Marionette', "\$bookmark->url() is '${metacpan_uri}pod/Firefox::Marionette':" . $bookmark->url()); ok($bookmark->date_added() == 1685610972, "\$bookmark->date_added() is " . localtime $bookmark->date_added()); ok($bookmark->last_modified() == 1685610973, "\$bookmark->last_modified() is " . localtime $bookmark->last_modified()); ok($bookmark->title() eq 'Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org', "\$bookmark->title() is 'Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org':" . $bookmark->title()); ok($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK(), "\$bookmark->type() is Firefox::Marionette::Bookmark::BOOKMARK():" . $bookmark->type()); ok($bookmark->parent_guid(), "\$bookmark->parent_guid() is " . $bookmark->parent_guid()); ok($bookmark->guid() eq 'utcxPgIOG05d', "\$bookmark->guid() is 'utcxPgIOG05d':" . $bookmark->guid()); my $count = 0; foreach my $bookmark ($firefox->bookmarks($metacpan_uri)) { $count += 1; } ok($count == 2, "\$firefox->search_bookmark('$metacpan_uri') produces 2 results:$count"); ok(ref $firefox->delete_bookmark($bookmark) eq $class, "\$firefox->delete_bookmark(\$bookmark) returns itself for chaining"); ok(!$firefox->bookmarks({ url => URI::URL->new($metacpan_uri . 'pod/Firefox::Marionette') }), "Bookmark for " . $bookmark->url() . " has been deleted"); ($bookmark) = $firefox->bookmarks({ url => URI::URL->new('https://perlmonks.org/') }); ok($bookmark->url() eq 'https://perlmonks.org/', "\$bookmark->url() is 'https://perlmonks.org/':" . $bookmark->url()); ok($bookmark->date_added() == 1686364081, "\$bookmark->date_added() is " . localtime $bookmark->date_added()); ok($bookmark->last_modified() == 1686364095, "\$bookmark->last_modified() is " . localtime $bookmark->last_modified()); ok($bookmark->title() eq 'PerlMonks - The Monastery Gates', "\$bookmark->title() is 'PerlMonks - The Monastery Gates':" . $bookmark->title()); ok($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK(), "\$bookmark->type() is Firefox::Marionette::Bookmark::BOOKMARK():" . $bookmark->type()); ok($bookmark->parent_guid(), "\$bookmark->parent_guid() is " . $bookmark->parent_guid()); ok($bookmark->guid(), "\$bookmark->guid() is " . $bookmark->guid()); ok(!defined $bookmark->icon_url(), "\$bookmark->icon_url() is not defined"); ok(!defined $bookmark->icon(), "\$bookmark->icon() is not defined"); $count = 0; foreach my $bookmark ($firefox->bookmarks($metacpan_uri)) { $count += 1; ok($bookmark->type(), "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a type method:" . $bookmark->type()); ok($bookmark->guid(), "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a guid method:" . $bookmark->guid()); ok($bookmark->url(), "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a url method:" . $bookmark->url()); my $title = $bookmark->title(); $title = Encode::encode('UTF-8', $title, 1); ok($title, "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a title method:" . $title); ok($bookmark->parent_guid(), "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a parent_guid method:" . $bookmark->parent_guid()); } ok($count == 1, "\$firefox->search_bookmark('$metacpan_uri') produces 1 result:$count"); $count = 0; foreach my $bookmark (reverse $firefox->bookmarks()) { $count += 1; ok($bookmark->type(), "\$firefox->search_bookmark() returns results ($count) that have a type method:" . $bookmark->type()); ok($bookmark->guid(), "\$firefox->search_bookmark() returns results ($count) that have a guid method:" . $bookmark->guid()); if ($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK()) { ok($bookmark->url(), "\$firefox->search_bookmark() returns results ($count) that have a url method:" . $bookmark->url()); } if (($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK()) || ($bookmark->type() == Firefox::Marionette::Bookmark::FOLDER())) { my $title = $bookmark->title(); $title = Encode::encode('UTF-8', $title, 1); ok($title, "\$firefox->search_bookmark() returns results ($count) that have a title method:" . $title); } my @tags = $bookmark->tags(); ok(scalar @tags >= 0, "\$firefox->search_bookmark() returns results ($count) that have a tag method that produces " . (scalar @tags) . " tags"); ok($bookmark->parent_guid(), "\$firefox->search_bookmark() returns results ($count) that have a parent_guid method:" . $bookmark->parent_guid()); if ($bookmark->parent_guid() ne Firefox::Marionette::Bookmark::ROOT()) { ok($firefox->delete_bookmark($bookmark), "Deleting this bookmark"); } } my $original_guid; ok($count >= 7, "\$firefox->search_bookmark('$metacpan_uri') produces more than 7 results:$count"); $bookmarks_path = File::Spec->catfile(Cwd::cwd(), qw(t data bookmarks_firefox.html)); ok(ref $firefox->import_bookmarks($bookmarks_path) eq $class, "\$firefox->import_bookmarks('$bookmarks_path') returns itself for chaining"); $count = 0; foreach my $bookmark (reverse $firefox->bookmarks()) { $count += 1; ok($bookmark->type(), "\$firefox->search_bookmark() returns results ($count) that have a type method:" . $bookmark->type()); ok($bookmark->guid(), "\$firefox->search_bookmark() returns results ($count) that have a guid method:" . $bookmark->guid()); ok(defined $bookmark->idx(), "\$firefox->search_bookmark() returns results ($count) that have a guid method:" . $bookmark->idx()); if ($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK()) { ok($bookmark->url(), "\$firefox->search_bookmark() returns results ($count) that have a url method:" . $bookmark->url()); } if (($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK()) || ($bookmark->type() == Firefox::Marionette::Bookmark::FOLDER())) { my $title = $bookmark->title(); $title = Encode::encode('UTF-8', $title, 1); ok($title, "\$firefox->search_bookmark() returns results ($count) that have a title method:" . $title); } ok($bookmark->parent_guid(), "\$firefox->search_bookmark() returns results ($count) that have a parent_guid method:" . $bookmark->parent_guid()); } ($bookmark) = $firefox->bookmarks({ url => URI::URL->new($metacpan_uri . 'pod/Firefox::Marionette') }); ok($bookmark, "Retrieved bookmark from firefox export as " . $bookmark->url()); ok($bookmark->url() eq $metacpan_uri . 'pod/Firefox::Marionette', "\$bookmark->url() is '${metacpan_uri}pod/Firefox::Marionette':" . $bookmark->url()); ok(defined $bookmark->idx(), "\$bookmark->idx() is defined:" . $bookmark->idx()); ok($bookmark->date_added() == 1685610972, "\$bookmark->date_added() is " . localtime $bookmark->date_added()); ok($bookmark->last_modified() == 1685610973, "\$bookmark->last_modified() is " . localtime $bookmark->last_modified()); ok($bookmark->content_type() eq 'text/x-moz-place', "\$bookmark->content_type() is 'text/x-moz-place':" . $bookmark->content_type()); ok($bookmark->title() eq 'Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org', "\$bookmark->title() is 'Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org':" . $bookmark->title()); ok($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK(), "\$bookmark->type() is Firefox::Marionette::Bookmark::BOOKMARK():" . $bookmark->type()); ok($bookmark->parent_guid(), "\$bookmark->parent_guid() is " . $bookmark->parent_guid()); if (!defined $original_guid) { $original_guid = $bookmark->guid(); } ok($bookmark->guid() eq $original_guid, "\$bookmark->guid() is '$original_guid':" . $bookmark->guid()); ok($bookmark->icon_url() eq $metacpan_bookmark_icon_url, "\$bookmark->icon_url() is '$metacpan_bookmark_icon_url':" . $bookmark->icon_url()); ok($bookmark->icon() eq $metacpan_bookmark_icon, "\$bookmark->icon() is correct:" . $bookmark->icon()); $count = 0; foreach my $bookmark ($firefox->bookmarks($metacpan_uri)) { $count += 1; } ok($count == 2, "\$firefox->search_bookmark('$metacpan_uri') produces 2 results:$count"); ok(ref $firefox->delete_bookmark($bookmark) eq $class, "\$firefox->delete_bookmark(\$bookmark) returns itself for chaining"); ok(!$firefox->bookmarks({ url => URI::URL->new($metacpan_uri . 'pod/Firefox::Marionette') }), "Bookmark for " . $bookmark->url() . " has been deleted"); ($bookmark) = $firefox->bookmarks({ url => URI::URL->new('https://perlmonks.org/') }); ok($bookmark->url() eq 'https://perlmonks.org/', "\$bookmark->url() is 'https://perlmonks.org/':" . $bookmark->url()); ok($bookmark->date_added() == 1686364081, "\$bookmark->date_added() is " . localtime $bookmark->date_added()); ok($bookmark->last_modified() == 1686364095, "\$bookmark->last_modified() is " . localtime $bookmark->last_modified()); ok($bookmark->title() eq 'PerlMonks - The Monastery Gates', "\$bookmark->title() is 'PerlMonks - The Monastery Gates':" . $bookmark->title()); ok($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK(), "\$bookmark->type() is Firefox::Marionette::Bookmark::BOOKMARK():" . $bookmark->type()); ok($bookmark->parent_guid(), "\$bookmark->parent_guid() is " . $bookmark->parent_guid()); ok($bookmark->guid(), "\$bookmark->guid() is " . $bookmark->guid()); ok(!defined $bookmark->icon_url(), "\$bookmark->icon_url() is not defined"); ok(!defined $bookmark->icon(), "\$bookmark->icon() is not defined"); $count = 0; foreach my $bookmark ($firefox->bookmarks($metacpan_uri)) { $count += 1; ok($bookmark->type(), "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a type method:" . $bookmark->type()); ok($bookmark->guid(), "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a guid method:" . $bookmark->guid()); ok($bookmark->url(), "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a url method:" . $bookmark->url()); my $title = $bookmark->title(); $title = Encode::encode('UTF-8', $title, 1); ok($title, "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a title method:" . $title); ok($bookmark->parent_guid(), "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a parent_guid method:" . $bookmark->parent_guid()); ok($bookmark->icon() eq $metacpan_bookmark_icon, "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a icon method that is correct:" . $bookmark->icon()); my $starting_regex = quotemeta 'fake-favicon-uri:https://metacpan.org'; ok($bookmark->icon_url() =~ /^$starting_regex/smx, "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a icon_url method that look vaguely correct:" . $bookmark->icon_url()); ok(!defined $bookmark->keyword(), "\$firefox->search_bookmark('$metacpan_uri') returns results ($count) that have a keyword method that returns undefined"); } ok($count == 1, "\$firefox->search_bookmark('$metacpan_uri') produces 1 result:$count"); my $icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADgElEQVQ4T6WTa0xbdRiHf/9259DT0pV2a7m4zo2OKTNjNo3TBNAtUbKQNigpQwOMLXHGTRN1yTAaNKCbW7yGqRnJdKibU2FkCMxZYFPi7FBhKW4tc4O2yCgUWmjp/fScHhm6TD/7fns/PL/kvTwE/7PIv/mxCoNCoOiyBKF20vFIIRVP0nFGEhYY+nea8IeYefZCdtdQbBESbnNLAYsdcT2xuYBk0G+ma+8xSUqrIMnbCMLz4LkE+EAAbF8bgvYBCxcIvpab/9sQaUDqFrsU4KzcvEm8QmNZWVabKb7XgKRnFNEL3Qi1tkKklkO+/RnIt1UAU07MfnlkLuwZNeWfvmpdCnBt2SIRrxe1qne+bEpGk/A17oXyxVcgK38W3GUrfO/UITk7DSEeg6axGTQtwsyppl7OM1qT2+nyktHqom0qQ+E5mXkvJswPQlpeC1XdYaT9M2TMfQ3+d+uQcF0HIhFkH/0OXNcn8NsGdg9yv7aQkeqii6tf/agwZu1BqPs0pE1t8MSl0KoZKDNkuDzFgfrgBWTY+5FMsJAbt2N5cQm8xw50RX/2mInD/EBYd6RdNrnrMSzLuhtM0xk4bvigVsqRr1Oi0+ZF5g/N0E2dRTwSR8rPIOfjbnjqa4KiYXs2cRk3CtqT/XBuzQNdoIfyYAug1IDiJpA2dx6iII/Yn4NgfcNgZwUE28exptuBmYYdmJq4IiPXyw3s2k8tlGePERyXgmz3fmSWmIHQWfC/PIWgVQNekEKkohC6JCDli2FVyzlMH9wzOWm7mkfsNUU/rn7ujUeig/1YOHMCTGkl7tr3FsDPg71yCmHLMdBEjOg8QXg4AHnZ01BsNWH6/X297HmnifxR9VDtCt19n8lq9mOyqhiUvhhZze2glx7Eg/nXa9Gzzo+1bgGakRlkfd2PVN9X8F/sfVz7xU+dZOxRg0KslZ5UV71kTAkUgu814GZjHY5Si+daCMI/7YZbk8Kh5hBKdzQBWcvhPf52n9/jqNB3uAN/f2K1fpM4I8eifvL5THqdHiPhMRye+xAkRVByU4lx5TKY1++C7sYEvB3H5xLjbpPuW9ulW07ccaFaXyBiVAcUGwxGUlKJ2EoF0lWZSIvzCDmGwH//DSLOaz2p0EJ9boHtvy7cNstasYrJluQ8zIKuF2Ls/Uw8ls7SaWySYQYocJ+z7ELHhjb74jrv2PgXU1mEH5MqexkAAAAASUVORK5CYII='; my $icon_url = 'https://duckduckgo.com/favicon.ico'; $count = 0; foreach my $bookmark ($firefox->bookmarks('https://duckduckgo.com')) { $count += 1; ok($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK(), "\$firefox->search_bookmark('https://duckduckgo.com') returns results ($count) that have a type that is Firefox::Marionette::Bookmark::BOOKMARK()::" . $bookmark->type()); ok($bookmark->guid(), "\$firefox->search_bookmark('https://duckduckgo.com') returns results ($count) that have a guid method:" . $bookmark->guid()); ok(ref $bookmark->url() eq 'URI::URL', "\$bookmark->url() returns a URI::URL object"); ok($bookmark->url() eq 'https://duckduckgo.com/?va=v&t=ha&q=perl+%F0%9F%90%AB&ia=web', "\$firefox->search_bookmark('https://duckduckgo.com') returns results ($count) that have a url that is 'https://duckduckgo.com/?va=v&t=ha&q=perl+%F0%9F%90%AB&ia=web':" . $bookmark->url()); my $title = $bookmark->title(); $title = Encode::encode('UTF-8', $title, 1); ok($title eq 'perl 🫠at DuckDuckGo', "\$firefox->search_bookmark('https://duckduckgo.com') returns results ($count) that have a title method that is 'perl 🫠at DuckDuckGo':" . $title); ok($bookmark->date_added() == 1685670771, "\$firefox->search_bookmark('https://duckduckgo.com') returns results ($count) that have a date_added method that is " . localtime $bookmark->date_added()); ok($bookmark->last_modified() == 1685670772, "\$firefox->search_bookmark('https://duckduckgo.com') returns results ($count) that have a last_modified method that is " . localtime $bookmark->last_modified()); ok($bookmark->parent_guid(), "\$firefox->search_bookmark('https://duckduckgo.com') returns results ($count) that have a parent_guid method:" . $bookmark->parent_guid()); ok(ref $bookmark->icon_url() eq 'URI::URL', "\$bookmark->icon_url() returns a URI::URL object"); ok($bookmark->icon_url() eq $icon_url, "\$firefox->search_bookmark('https://duckduckgo.com') returns results ($count) that have a icon_url method that is '$icon_url':" . $bookmark->icon_url()); ok(ref $bookmark->icon() eq 'URI::data', "\$bookmark->icon() returns a URI::data object"); ok($bookmark->icon() eq $icon, "\$firefox->search_bookmark('https://duckduckgo.com') returns results ($count) that have a icon method that is :" . $bookmark->icon()); my $keyword = $bookmark->keyword(); $keyword = Encode::encode('UTF-8', $keyword, 1); ok($keyword eq 'ðŸ«', "\$firefox->search_bookmark('https://duckduckgo.com') returns results ($count) that have a keyword method:" . $keyword); my $tag_count = 0; foreach my $tag ($bookmark->tags()) { $tag_count += 1; $tag = Encode::encode('UTF-8', $tag, 1); ok($tag =~ /^(ðŸ«|ddg|UTF8|perl)$/smx, "\$firefox->search_bookmark('https://duckduckgo.com') returns results ($count) that have a tag method with one of 'ðŸ«', 'ddg', 'UTF8' or 'perl':" . $tag); } ok($tag_count == 4, "\$firefox->search_bookmark('https://duckduckgo.com') returns results ($count) that have a tag method with 4 tags:" . $tag_count); } ok($count == 1, "\$firefox->search_bookmark('https://duckduckgo.com') produces 1 result:$count"); $count = 0; foreach my $bookmark (reverse $firefox->bookmarks()) { $count += 1; if ($bookmark->parent_guid() ne Firefox::Marionette::Bookmark::ROOT()) { ok($firefox->delete_bookmark($bookmark), "Deleting this bookmark:" . $bookmark->guid()); } } $bookmarks_path = File::Spec->catfile(Cwd::cwd(), qw(t data bookmarks_firefox.json)); ok(ref $firefox->import_bookmarks($bookmarks_path) eq $class, "\$firefox->import_bookmarks('$bookmarks_path') returns itself for chaining"); $count = 0; foreach my $bookmark (reverse $firefox->bookmarks()) { $count += 1; } my $original_count = $count; my $metacpan_pod_guid = 'utcxPgIOG05d'; ok($firefox->delete_bookmark(Firefox::Marionette::Bookmark->new(guid => $metacpan_pod_guid)), "Deleting this bookmark:" . $metacpan_pod_guid); ok(ref $firefox->import_bookmarks($bookmarks_path) eq $class, "\$firefox->import_bookmarks('$bookmarks_path') returns itself for chaining"); $count = 0; foreach my $bookmark (reverse $firefox->bookmarks()) { if (my $title = $bookmark->title()) { $title = Encode::encode('UTF-8', $title, 1); ok($title, "Bookmark $count is " . $title); } else { ok($bookmark->guid(), "Bookmark $count is " . $bookmark->guid() . " with type " . $bookmark->type() . " with parent " . $bookmark->parent_guid()); } $count += 1; } ok($original_count == $count, "Same number of bookmarks after importing the same json file:$original_count == $count"); ($bookmark) = $firefox->bookmarks({ url => URI::URL->new($metacpan_uri . 'pod/Firefox::Marionette') }); ok($bookmark->url() eq $metacpan_uri . 'pod/Firefox::Marionette', "\$bookmark->url() is '${metacpan_uri}pod/Firefox::Marionette':" . $bookmark->url()); ok(defined $bookmark->idx(), "\$bookmark->idx() is defined:" . $bookmark->idx()); ok($bookmark->date_added() == 1685610972, "\$bookmark->date_added() is " . localtime $bookmark->date_added()); ok($bookmark->last_modified() == 1685610973, "\$bookmark->last_modified() is " . localtime $bookmark->last_modified()); ok($bookmark->content_type() eq 'text/x-moz-place', "\$bookmark->content_type() is 'text/x-moz-place':" . $bookmark->content_type()); ok($bookmark->title() eq 'Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org', "\$bookmark->title() is 'Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org':" . $bookmark->title()); ok($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK(), "\$bookmark->type() is Firefox::Marionette::Bookmark::BOOKMARK():" . $bookmark->type()); ok($bookmark->parent_guid(), "\$bookmark->parent_guid() is " . $bookmark->parent_guid()); ok($bookmark->guid() eq $metacpan_pod_guid, "\$bookmark->guid() is '$metacpan_pod_guid':" . $bookmark->guid()); ok($bookmark->icon_url() eq $metacpan_bookmark_icon_url, "\$bookmark->icon_url() is '$metacpan_bookmark_icon_url':" . $bookmark->icon_url()); ok($bookmark->icon() eq $metacpan_bookmark_icon, "\$bookmark->icon() is correct:" . $bookmark->icon()); $bookmarks_path = File::Spec->catfile(Cwd::cwd(), qw(t data bookmarks_firefox.html)); ok(ref $firefox->import_bookmarks($bookmarks_path) eq $class, "\$firefox->import_bookmarks('$bookmarks_path') returns itself for chaining"); $count = 0; foreach my $bookmark (reverse $firefox->bookmarks()) { if (my $title = $bookmark->title()) { $title = Encode::encode('UTF-8', $title, 1); ok($title, "Bookmark $count is " . $title); } else { ok($bookmark->guid(), "Bookmark $count is " . $bookmark->guid() . " with type " . $bookmark->type() . " with parent " . $bookmark->parent_guid()); } $count += 1; } ok($original_count == $count, "Same number of bookmarks after importing the same json file:$original_count == $count"); ($bookmark) = $firefox->bookmarks({ url => URI::URL->new($metacpan_uri . 'pod/Firefox::Marionette') }); ok($bookmark->url() eq $metacpan_uri . 'pod/Firefox::Marionette', "\$bookmark->url() is '${metacpan_uri}pod/Firefox::Marionette':" . $bookmark->url()); ok(defined $bookmark->idx(), "\$bookmark->idx() is defined:" . $bookmark->idx()); ok($bookmark->date_added() == 1685610972, "\$bookmark->date_added() is " . localtime $bookmark->date_added()); ok($bookmark->last_modified() == 1685610973, "\$bookmark->last_modified() is " . localtime $bookmark->last_modified()); ok($bookmark->content_type() eq 'text/x-moz-place', "\$bookmark->content_type() is 'text/x-moz-place':" . $bookmark->content_type()); ok($bookmark->title() eq 'Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org', "\$bookmark->title() is 'Firefox::Marionette - Automate the Firefox browser with the Marionette protocol - metacpan.org':" . $bookmark->title()); ok($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK(), "\$bookmark->type() is Firefox::Marionette::Bookmark::BOOKMARK():" . $bookmark->type()); ok($bookmark->parent_guid(), "\$bookmark->parent_guid() is " . $bookmark->parent_guid()); ok($bookmark->guid() eq $metacpan_pod_guid, "\$bookmark->guid() is '$metacpan_pod_guid':" . $bookmark->guid()); ok($bookmark->icon_url() eq $metacpan_bookmark_icon_url, "\$bookmark->icon_url() is '$metacpan_bookmark_icon_url':" . $bookmark->icon_url()); ok($bookmark->icon() eq $metacpan_bookmark_icon, "\$bookmark->icon() is correct:" . $bookmark->icon()); $bookmarks_path = File::Spec->catfile(Cwd::cwd(), qw(t data bookmarks_firefox.html)); ok(ref $firefox->import_bookmarks($bookmarks_path) eq $class, "\$firefox->import_bookmarks('$bookmarks_path') returns itself for chaining"); $count = 0; foreach my $bookmark (reverse $firefox->bookmarks()) { if (my $title = $bookmark->title()) { my $title = $bookmark->title(); $title = Encode::encode('UTF-8', $title, 1); ok($title, "Bookmark $count is " . $title); } else { ok($bookmark->guid(), "Bookmark $count is " . $bookmark->guid() . " with type " . $bookmark->type() . " with parent " . $bookmark->parent_guid()); } $count += 1; } ok($original_count == $count, "Same number of bookmarks after importing the same json file:$original_count == $count"); $count = 0; foreach my $bookmark (reverse $firefox->bookmarks()) { $count += 1; ok($bookmark->type(), "\$firefox->search_bookmark() returns results ($count) that have a type method:" . $bookmark->type()); ok($bookmark->guid(), "\$firefox->search_bookmark() returns results ($count) that have a guid method:" . $bookmark->guid()); if ($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK()) { ok($bookmark->url(), "\$firefox->search_bookmark() returns results ($count) that have a url method:" . $bookmark->url()); } ok($bookmark->content_type(), "\$firefox->search_bookmark() returns results ($count) that have a content_type method:" . $bookmark->content_type()); if (($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK()) || ($bookmark->type() == Firefox::Marionette::Bookmark::FOLDER())) { my $title = $bookmark->title(); $title = Encode::encode('UTF-8', $title, 1); ok($title, "\$firefox->search_bookmark() returns results ($count) that have a title method:" . $title); } if ($bookmark->type() == Firefox::Marionette::Bookmark::BOOKMARK()) { if (defined $bookmark->icon_url()) { ok($bookmark->icon_url(), "\$firefox->search_bookmark() returns results ($count) that have a icon_url method:" . $bookmark->icon_url()); } if (defined $bookmark->icon()) { ok($bookmark->icon(), "\$firefox->search_bookmark() returns results ($count) that have a icon method that is :" . $bookmark->icon()); } } ok($bookmark->parent_guid(), "\$firefox->search_bookmark() returns results ($count) that have a parent_guid method:" . $bookmark->parent_guid()); if ($bookmark->parent_guid() ne Firefox::Marionette::Bookmark::ROOT()) { ok($firefox->delete_bookmark($bookmark), "Deleting this bookmark"); } } ok($count >= 7, "\$firefox->search_bookmark('$metacpan_uri') produces more than 7 results:$count"); $bookmark = Firefox::Marionette::Bookmark->new( parent_guid => Firefox::Marionette::Bookmark::MOBILE(), url => URI::URL->new($metacpan_uri), icon => URI::data->new($metacpan_bookmark_icon), icon_url => URI::URL->new($metacpan_bookmark_icon_url) ); ok($bookmark, "Firefox::Marionette::Bookmark->new() produces a new bookmark"); ok(ref $bookmark->url() eq 'URI::URL', "Firefox::Marionette::Bookmark->new()->url() returns a URI::URL object"); ok($bookmark->url() eq $metacpan_uri, "Firefox::Marionette::Bookmark->new()->url() returns '$metacpan_uri' as a string"); ok(ref $bookmark->icon_url() eq 'URI::URL', "Firefox::Marionette::Bookmark->new()->icon_url() returns a URI::URL object"); ok($bookmark->icon_url() eq $metacpan_bookmark_icon_url, "Firefox::Marionette::Bookmark->new()->icon_url() returns '$metacpan_bookmark_icon_url' as a string"); ok(ref $bookmark->icon() eq 'URI::data', "Firefox::Marionette::Bookmark->new()->icon() returns a URI::data object"); ok($bookmark->icon_url() eq $metacpan_bookmark_icon_url, "Firefox::Marionette::Bookmark->new()->icon() returns '$metacpan_bookmark_icon' as a string"); my $bookmark_name = "Meta Cpan for the win!"; $bookmark = Firefox::Marionette::Bookmark->new( title => $bookmark_name, url => $metacpan_uri, icon => $metacpan_bookmark_icon, icon_url => $metacpan_bookmark_icon_url ); ok($bookmark, "Firefox::Marionette::Bookmark->new() produces a new bookmark"); ok($bookmark->title() eq $bookmark_name, "Firefox::Marionette::Bookmark->new()->title() returns '$bookmark_name' as a string"); ok(ref $bookmark->url() eq 'URI::URL', "Firefox::Marionette::Bookmark->new()->url() returns a URI::URL object"); ok($bookmark->url() eq $metacpan_uri, "Firefox::Marionette::Bookmark->new()->url() returns '$metacpan_uri' as a string"); ok(ref $bookmark->icon_url() eq 'URI::URL', "Firefox::Marionette::Bookmark->new()->icon_url() returns a URI::URL object"); ok($bookmark->icon_url() eq $metacpan_bookmark_icon_url, "Firefox::Marionette::Bookmark->new()->icon_url() returns '$metacpan_bookmark_icon_url' as a string"); ok(ref $bookmark->icon() eq 'URI::data', "Firefox::Marionette::Bookmark->new()->icon() returns a URI::data object"); ok($bookmark->icon_url() eq $metacpan_bookmark_icon_url, "Firefox::Marionette::Bookmark->new()->icon() returns '$metacpan_bookmark_icon' as a string"); ok($bookmark->type() eq Firefox::Marionette::Bookmark::BOOKMARK(), "Firefox::Marionette::Bookmark->new()->type() returns a bookmark if a url and title is supplied"); ok($bookmark->content_type() eq 'text/x-moz-place', "Firefox::Marionette::Bookmark->new()->content_type() returns 'text/x-moz-place' if a url and title is supplied"); my $folder_name = "Samples"; $bookmark = Firefox::Marionette::Bookmark->new( title => $folder_name ); ok($bookmark->title() eq $folder_name, "Firefox::Marionette::Bookmark->new()->title() returns the supplied title"); ok($bookmark->type() eq Firefox::Marionette::Bookmark::FOLDER(), "Firefox::Marionette::Bookmark->new()->type() returns a folder if a title is supplied, with no url"); ok($bookmark->content_type() eq 'text/x-moz-place-container', "Firefox::Marionette::Bookmark->new()->content_type() returns 'text/x-moz-place-container' if a title is supplied, with no url"); $bookmark = Firefox::Marionette::Bookmark->new( type => 4, index => undef ); $bookmark = Firefox::Marionette::Bookmark->new( type => 4, index => undef ); ok($bookmark, "Firefox::Marionette::Bookmark->new() produces a new bookmark with an unknown type of 4"); ok(!defined $bookmark->content_type(), "If a bookmark does not have a known type, it does not have a content type"); ok(JSON->new()->convert_blessed()->encode($bookmark), "Bookmark with bad parameters can be turned into JSON:" . JSON->new()->convert_blessed()->encode($bookmark)); $bookmarks_path = File::Spec->catfile(Cwd::cwd(), qw(t data bookmarks_truncated.html)); eval { $firefox->import_bookmarks($bookmarks_path); }; chomp $@; ok($@, "Exception correctly thrown when trying to import a truncated bookmarks file of $bookmarks_path:$@"); } my $bookmark = Firefox::Marionette::Bookmark->new(); ok(!defined $bookmark->type(), "There is no defined type if a bookmark is created with no parameters"); ok(!defined $bookmark->content_type(), "There is no defined content_type if a bookmark is created with no parameters"); Firefox::Marionette::Bookmark->import(qw(:all)); foreach my $name (qw(BOOKMARK FOLDER SEPARATOR)) { my $result = eval "return Firefox::Marionette::Bookmark::$name();"; no strict; ok($result == &$name(), "Firefox::Marionette::Bookmark::$name() == $name() after Firefox::Marionette::Bookmark->import(:all)"); use strict; } foreach my $name (qw(MENU ROOT TOOLBAR TAGS UNFILED)) { my $result = eval "return Firefox::Marionette::Bookmark::$name();"; no strict; ok($result eq &$name(), "Firefox::Marionette::Bookmark::$name() eq $name() after Firefox::Marionette::Bookmark->import(:all)"); use strict; } my $new_max_url_length = 4321; my $original_max_url_length = $firefox->get_pref('browser.history.maxStateObjectSize'); ok($original_max_url_length =~ /^\d+$/smx, "Retrieved browser.history.maxStateObjectSize as a number '$original_max_url_length'"); ok(ref $firefox->set_pref('browser.history.maxStateObjectSize', $new_max_url_length) eq $class, "\$firefox->set_pref correctly returns itself for chaining and set 'browser.history.maxStateObjectSize' to '$new_max_url_length'"); my $max_url_length = $firefox->get_pref('browser.history.maxStateObjectSize'); ok($max_url_length == $new_max_url_length, "Retrieved browser.history.maxStateObjectSize which was equal to the previous setting of '$new_max_url_length'"); ok(ref $firefox->set_pref('browser.history.maxStateObjectSize', $original_max_url_length) eq $class, "\$firefox->set_pref correctly returns itself for chaining and set 'browser.history.maxStateObjectSize' to the original '$original_max_url_length'"); $max_url_length = $firefox->get_pref('browser.history.maxStateObjectSize'); ok($max_url_length == $original_max_url_length, "Retrieved browser.history.maxStateObjectSize as a number '$max_url_length' which was equal to the original setting of '$original_max_url_length'"); my $original_use_system_colours = $firefox->get_pref('browser.display.use_system_colors'); ok($original_use_system_colours =~ /^[01]$/smx, "Retrieved browser.display.use_system_colors as a boolean '$original_use_system_colours', and set it as true"); ok(ref $firefox->set_pref('browser.display.use_system_colors', \1) eq $class, "\$firefox->set_pref correctly returns itself for chaining and set 'browser.display.use_system_colors' to 'true'");; my $use_system_colours = $firefox->get_pref('browser.display.use_system_colors'); ok($use_system_colours, "Retrieved browser.display.use_system_colors as true '$use_system_colours'"); ok(ref $firefox->set_pref('browser.display.use_system_colors', \0) eq $class, "\$firefox->set_pref correctly returns itself for chaining and set 'browser.display.use_system_colors' to 'false'");; $use_system_colours = $firefox->get_pref('browser.display.use_system_colors'); ok(!$use_system_colours, "Retrieved browser.display.use_system_colors as false '$use_system_colours'"); ok(ref $firefox->clear_pref('browser.display.use_system_colors', \0) eq $class, "\$firefox->clear_pref correctly returns itself for chaining and cleared 'browser.display.use_system_colors'"); $use_system_colours = $firefox->get_pref('browser.display.use_system_colors'); ok($use_system_colours == $original_use_system_colours, "Retrieved original browser.display.use_system_colors as a boolean '$use_system_colours'"); ok(!defined $firefox->get_pref('browser.no_such_key'), "Returned undef when querying for a non-existant key of 'browser.no_such_key'"); my $new_value = "Can't be real:$$"; ok(ref $firefox->set_pref('browser.no_such_key', $new_value) eq $class, "\$firefox->set_pref correctly returns itself for chaining and set 'browser.no_such_key' to '$new_value'"); ok($firefox->get_pref('browser.no_such_key') eq $new_value, "Returned browser.no_such_key as a string '$new_value'"); my $new_active_colour = '#FFFFFF'; my $original_active_colour = $firefox->get_pref('browser.active_color'); ok($original_active_colour =~ /^[#][[:xdigit:]]{6}$/smx, "Retrieved browser.active_color as a string '$original_active_colour'"); my $active_colour = $firefox->get_pref('browser.active_color'); ok($active_colour eq $original_active_colour, "Retrieved browser.active_color as a string '$active_colour' which was equal to the original setting of '$original_active_colour'"); ok(ref $firefox->set_pref('browser.active_color', $new_active_colour) eq $class, "\$firefox->set_pref correctly returns itself for chaining and set 'browser.active_color' to '$new_active_colour'");; $active_colour = $firefox->get_pref('browser.active_color'); ok($active_colour eq $new_active_colour, "Retrieved browser.active_color as a string '$active_colour' which was equal to the new setting of '$new_active_colour'"); ok(ref $firefox->clear_pref('browser.active_color') eq $class, "\$firefox->clear_pref correctly returns itself for chaining and cleared 'browser.active_color'");; $active_colour = $firefox->get_pref('browser.active_color'); ok($active_colour eq $original_active_colour, "Retrieved browser.active_color as a string '$active_colour' which was equal to the original string of '$original_active_colour'"); 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 (!$ENV{RELEASE_TESTING}) { skip("Skipping network tests", 2); } if (grep /^accept_insecure_certs$/, $capabilities->enumerate()) { ok(!$capabilities->accept_insecure_certs(), "\$capabilities->accept_insecure_certs() is false"); if (($ENV{FIREFOX_HOST}) && ($ENV{FIREFOX_HOST} ne 'localhost')) { diag("insecure cert test is not supported for remote hosts"); } elsif (($ENV{FIREFOX_HOST}) && ($ENV{FIREFOX_HOST} eq 'localhost') && ($ENV{FIREFOX_PORT})) { diag("insecure cert test is not supported for remote hosts"); } elsif ((exists $Config::Config{'d_fork'}) && (defined $Config::Config{'d_fork'}) && ($Config::Config{'d_fork'} eq 'define')) { my $ip_address = '127.0.0.1'; my $daemon = IO::Socket::SSL->new( LocalAddr => $ip_address, LocalPort => 0, Listen => 20, SSL_cert_file => $ca_cert_handle->filename(), SSL_key_file => $ca_private_key_handle->filename(), ); my $url = "https://$ip_address:" . $daemon->sockport(); if (my $pid = fork) { wait_for_server_on($daemon, $url, $pid); eval { $firefox->go(URI->new($url)) }; my $exception = "$@"; chomp $exception; ok(ref $@ eq 'Firefox::Marionette::Exception::InsecureCertificate', $url . " threw an exception:$exception"); 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 insecure cert test\n" }; alarm 40; $0 = "[Test insecure cert test for " . getppid . "]"; diag("Accepting connections on $url for $0"); foreach ((1 .. 3)) { my $connection = $daemon->accept(); } exit 0; }; chomp $@; diag("insecure cert test server failed:$@"); exit 1; } else { diag("insecure cert test fork failed:$@"); } } else { diag("No forking available for $^O"); } } 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 t/data/keepassxs.csv)) { my $handle = FileHandle->new($path, Fcntl::O_RDONLY()) or die "Failed to open $path:$!"; my @logins; my $encoded_username = '!"§$%&/()=?`´²³{[]}\\'; my $display_username = $encoded_username; my $utf8_username = Encode::decode('UTF-8', $encoded_username, 1); my $found_utf8_user; foreach my $login (Firefox::Marionette->logins_from_csv($handle)) { if ($path eq 't/data/keepassxs.csv') { if ($login->user() eq $utf8_username) { $found_utf8_user = 1; my $encoded_password = 'öüäÖÜÄß<>@€µ®“«'; my $utf8_password = Encode::decode('UTF-8', $encoded_password, 1); ok($login->password() eq $utf8_password, "$display_username contains a correctly encoded UTF-8 password"); ok($login->creation_time() == 1644485034, "$display_username has a creation time of " . gmtime($login->creation_time())); ok($login->password_changed_time() == 1644398823, "$display_username has a password changed time of " . gmtime($login->password_changed_time())); } } else { 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; } if ($path eq 't/data/keepassxs.csv') { ok($found_utf8_user, "$path contains a UTF-8 username of $display_username for $path"); } 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; my $encoded_username = 'tésting@au.example.org'; my $display_username = $encoded_username; my $utf8_username = Encode::decode('UTF-8', $encoded_username, 1); my $found_utf8_user; 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)); if ($login->user() eq $utf8_username) { $found_utf8_user = 1; my $encoded_password = 'TGe3xQxzZ8t4nfzQ-vpY6@D4GnCQaFTuD3hDe72D3btt!'; my $utf8_password = Encode::decode('UTF-8', $encoded_password, 1); ok($login->password() eq $utf8_password, "$display_username contains a correctly encoded UTF-8 password"); ok($login->creation_time() == 1641413610, "$display_username has a creation time of " . gmtime($login->creation_time())); ok($login->password_changed_time() == 1641850061, "$display_username has a password changed time of " . gmtime($login->password_changed_time())); } 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($found_utf8_user, "$path contains a UTF-8 username of $display_username for $path"); 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"); foreach my $path (qw(t/data/keepass1.xml)) { my $handle = FileHandle->new($path, Fcntl::O_RDONLY()) or die "Failed to open $path:$!"; my @logins; my $encoded_username = '!"§$%&/()=?`´²³{[]}\\'; my $display_username = $encoded_username; my $utf8_username = Encode::decode('UTF-8', $encoded_username, 1); my $found_utf8_user; foreach my $login (Firefox::Marionette->logins_from_xml($handle)) { ok($login->host() =~ /^https?:\/\/(?:[a-z]+[.])?[a-z]+[.](?:com|net|org)(?:[:]\d+)?\/?$/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)); if ($login->user() eq $utf8_username) { $found_utf8_user = 1; my $encoded_password = 'öüäÖÜÄß<>@€µ®“«'; my $utf8_password = Encode::decode('UTF-8', $encoded_password, 1); ok($login->password() eq $utf8_password, "$display_username contains a correctly encoded UTF-8 password"); ok($login->creation_time() == 1167566157, "$display_username has a creation time of " . gmtime($login->creation_time())); ok($login->password_changed_time() == 1167566166, "$display_username has a password changed time of " . gmtime($login->password_changed_time())); } 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($found_utf8_user, "$path contains a UTF-8 username of $display_username for $path"); 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_xml($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"); my $test_login = Firefox::Marionette::Login->new(host => 'https://github.com', user => 'ddick@cpan.org', password => 'qwerty', user_field => 'login', password_field => 'password', creation_in_ms => 0, last_used_in_ms => undef); ok(!defined $test_login->last_used_time(), "Firefox::Marionette::Login->new()->last_used_time() correctly returns undef for an undefined parameter to new"); ok($test_login->creation_time() == 0, "Firefox::Marionette::Login->new()->creation_time() correctly returns 0 for a 0 parameter to new"); TODO: { local $TODO = $correct_exit_status == 0 ? q[] : "$version_string is not exiting cleanly"; 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"); my %extra_parameters; my $visible = 0; if ($nightly || $developer) { $visible = 1; $extra_parameters{visible} = $visible; diag("Forcing visible for nightly firefox to dodge issues with geo/har methods"); } ($skip_message, $firefox) = start_firefox($visible, har => 1, debug => 0, capabilities => Firefox::Marionette::Capabilities->new(moz_headless => 1), geo => URI->new($freeipapi_uri), %extra_parameters); 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"); if ($major_version >= 60) { my $title = "This is MetaCPAN!"; my $bookmark = Firefox::Marionette::Bookmark->new( url => $metacpan_uri, title => $title); ok($firefox->add_bookmark($bookmark), "Added a new bookmark to firefox"); TODO: { local $TODO = $major_version <= 80 && $major_version >= 70 ? "Temporary problems with bookmarks" : q[]; my $guid; my $count = 0; foreach my $result ($firefox->bookmarks($metacpan_uri)) { ok($result->url() eq $metacpan_uri, "Retrieved the correct bookmark"); $guid = $result->guid(); ok($guid, "Found $guid as the guid of the new bookmark"); $count += 1; } ok($count == 1, "Total number of bookmarks found when searching by url is 1:$count"); $count = 0; foreach my $result ($firefox->bookmarks(url => $metacpan_uri)) { ok($result->url() eq $metacpan_uri, "Retrieved the correct bookmark"); $count += 1; } ok($count == 1, "Total number of bookmarks found when searching by (url => '$metacpan_uri') is 1:$count"); $count = 0; foreach my $result ($firefox->bookmarks(url => URI->new($metacpan_uri))) { ok($result->url() eq $metacpan_uri, "Retrieved the correct bookmark"); $count += 1; } ok($count == 1, "Total number of bookmarks found when searching by (url => URI->new('$metacpan_uri')) is 1:$count"); $count = 0; foreach my $result ($firefox->bookmarks($title)) { ok($result->url() eq $metacpan_uri, "Retrieved the correct bookmark"); $count += 1; } ok($count == 1, "Total number of bookmarks found when searching by title is 1:$count"); $count = 0; foreach my $result ($firefox->bookmarks(title => $title)) { ok($result->url() eq $metacpan_uri, "Retrieved the correct bookmark"); $count += 1; } ok($count == 1, "Total number of bookmarks found when searching by (title => '$title') is 1:$count"); } } 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}) && (!$ENV{FIREFOX_NO_NETWORK})) { # har sometimes hangs and sometimes metacpan.org fails certificate checks. for example. http://www.cpantesters.org/cpan/report/e71bfb3b-7413-1014-98e6-045206f7812f if (!$tls_tests_ok) { skip("TLS test infrastructure seems compromised", 5); } if ($^O eq 'darwin') { if (($firefox->nightly()) || ($firefox->developer())) { skip("github actions are having trouble for Darwin nightly", 5); diag("github actions are having trouble for Darwin nightly"); } } 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); } if ($major_version >= $min_geo_version) { my $geo6; eval { $geo6 = $firefox->geo(); } or do { chomp $@; diag("Threw an exception in geo method:$@"); }; if ((!defined $geo6) && (($uname eq 'cygwin') || ($uname eq 'MSWin32'))) { diag("Location services may be disabled"); eval { $firefox->dismiss_alert(); }; } elsif (defined $geo6) { ok($geo6->latitude() == -37.5, "\$firefox->geo()->latitude() returned -31.5:" . $geo6->latitude()); ok($geo6->longitude() == 144.5, "\$firefox->geo()->longitude() returned 144.5:" . $geo6->longitude()); } } 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 (($uname eq 'cygwin') || ($uname eq 'MSWin32') || ($uname eq 'FreeBSD')) { TODO: { local $TODO = "$uname 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"); $profile->set_value('general.useragent.override', 'libwww-perl/6.72'); ($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'), geo => $freeipapi_uri); 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 $agent; eval { $agent = $firefox->agent( os => 'Linux' ); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->agent(os => 'Linux') fails when general.useragent.override is already set to an unparsable value:$@"); 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:$@"); }; if ($ENV{FIREFOX_HOST}) { } elsif (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { diag("Skipping checks that use a file:// url b/c of OpenBSD's unveil functionality - see https://bugzilla.mozilla.org/show_bug.cgi?id=1580271"); } else { # Coping with OpenBSD unveil - see https://bugzilla.mozilla.org/show_bug.cgi?id=1580271 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); } if ($major_version >= $min_geo_version) { if (my $geo = $firefox->geo()) { ok($geo->latitude() == -37.5, "\$firefox->geo()->latitude() returned -31.5:" . $geo->latitude()); ok($geo->longitude() == 144.5, "\$firefox->geo()->longitude() returned 144.5:" . $geo->longitude()); } elsif (($uname eq 'cygwin') || ($uname eq 'MSWin32')) { diag("Location services may be disabled"); eval { $firefox->dismiss_alert(); }; } } if ($major_version >= 121) { my @frames = sort @{$firefox->script("return [ window.frames[0], window.frames[1] ];")}; ok($frames[0]->isa('Firefox::Marionette::WebFrame') && $frames[1]->isa('Firefox::Marionette::WebFrame') && (scalar @frames == 2), "An array from javascript of frames returns WebFrame objects"); } 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); } 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 GUIDs:" . $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 GUIDs:" . $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 GUIDs:" . $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"); if ($major_version >= 121) { my $script_window = $firefox->script("return window"); my $initial_url = $firefox->script("return window.location.href"); ok($script_window->isa('Firefox::Marionette::WebFrame'), "\$firefox->script(\"return window\") returns a Firefox::Marionette::Frame object"); my $argument_url = $firefox->script("return arguments[0].location.href", args => [ $script_window ]); ok($argument_url eq $initial_url, "window object can be used as an in and out parameter for javascript calls:$argument_url:$initial_url"); } } 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"); } my $browser_name = $firefox->capabilities()->browser_name(); 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 || $browser_name =~ /waterfox/i ? "\$firefox->close_current_chrome_window_handle() can return a undef value for versions less than 52 or browser is waterfox" : 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"); } if (!($ENV{RELEASE_TESTING}) || ($ENV{FIREFOX_NO_NETWORK})) { skip("Skipping network tests", 225); } ok($firefox->refresh(), "\$firefox->refresh()"); 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(); if ($uri eq $metacpan_uri) { ok($uri =~ /metacpan/smx, "\$firefox->uri() contains /metacpan/:$uri"); } else { 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() =~ /Search[ ]the[ ]CPAN/smx, "metacpan.org contains the phrase 'Search the CPAN' in page source"); ok($firefox->html() =~ /Search[ ]the[ ]CPAN/smx, "metacpan.org contains the phrase 'Search the CPAN' in html"); 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"); my $number_of_links = 0; 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 @scroll_arguments = test_scroll_arguments($number_of_links++); ok($link->scroll(@scroll_arguments), "Firefox scrolled to the link with arguments of:" . join q[, ], stringify_scroll_arguments(@scroll_arguments)); } 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('Firefox::Marionette'), "Sent 'Firefox::Marionette' 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($page_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($page_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($page_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($page_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($page_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($page_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($page_content)->find_id($search_box_id)->attribute('id') eq $search_box_id, "Correctly found nested element with find_id"); ok($firefox->has_class($page_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($page_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($page_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($page_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($page_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($page_content)->find_name('q')->attribute('id') eq $search_box_id, "Correctly found nested element with find_name"); ok($firefox->has_class($page_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($page_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($page_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($page_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($page_content)->find_tag('input')->attribute('id'), "Correctly found nested element with find_tag"); ok($firefox->has_class($page_content)->has_tag('input')->attribute('id'), "Correctly found nested element with has_tag"); $count = 0; foreach my $element ($firefox->find_by_class($page_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($page_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, 'class name')->attribute('id'), "Correctly found element when searching by class name"); ok($firefox->find($form_control, BY_CLASS())->attribute('id'), "Correctly found element when searching by class name"); ok($firefox->list_by_class($form_control)->attribute('id'), "Correctly found element with list_by_class"); ok($firefox->find_by_class($form_control)->attribute('id'), "Correctly found element with find_by_class"); ok($firefox->find_by_class($page_content)->find_by_class($form_control)->attribute('id'), "Correctly found nested element with find_by_class"); ok($firefox->find_class($form_control)->attribute('id'), "Correctly found element with find_class"); ok($firefox->find_class($page_content)->find_class($form_control)->attribute('id'), "Correctly found nested element with find_class"); ok($firefox->has_class($page_content)->has_class($form_control)->attribute('id'), "Correctly found nested element with has_class"); ok(!defined $firefox->has_class($page_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($page_content)->list_by_class($form_control)) { 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($page_content)->find_by_class($form_control)) { 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($page_content)->find_class($form_control)) { 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($css_form_control, 'css selector')->attribute('id'), "Correctly found element when searching by css selector"); ok($firefox->find($css_form_control, BY_SELECTOR())->attribute('id'), "Correctly found element when searching by css selector"); ok($firefox->list_by_selector($css_form_control)->attribute('id'), "Correctly found element with list_by_selector"); ok($firefox->find_by_selector($css_form_control)->attribute('id'), "Correctly found element with find_by_selector"); ok($firefox->find_by_class($page_content)->find_by_selector($css_form_control)->attribute('id'), "Correctly found nested element with find_by_selector"); ok($firefox->find_selector($css_form_control)->attribute('id'), "Correctly found element with find_selector"); ok($firefox->find_class($page_content)->find_selector($css_form_control)->attribute('id'), "Correctly found nested element with find_selector"); ok($firefox->has_class($page_content)->has_selector($css_form_control)->attribute('id'), "Correctly found nested element with has_selector"); $count = 0; foreach my $element ($firefox->find_by_class($page_content)->list_by_selector($css_form_control)) { 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($page_content)->find_by_selector($css_form_control)) { 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($css_form_control)) { 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($css_form_control)) { 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($css_form_control)) { 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($footer_links)->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($footer_links)->find_link('API')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx; }; ok($result, "Correctly found nested element with find_link"); eval { $result = $firefox->has_class($footer_links)->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($footer_links)->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($footer_links)->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); } if ($major_version >= 61) { ok($count == 1, "Found elements with nested find_by_link:$count"); } else { ok((($count == 1) or ($count == 2)), "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 { if ($major_version >= 61) { ok($count == 1, "Found elements with wantarray find_by_link:$count"); } else { ok((($count == 1) or ($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 { if ($major_version >= 61) { ok($count == 1, "Found elements with wantarray find_link:$count"); } else { ok((($count == 1) or ($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($footer_links)->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($footer_links)->find_partial('AP')->attribute('href') =~ /^https:\/\/fastapi[.]metacpan[.]org\/?$/smx, "Correctly found nested element with find_partial"); ok($firefox->has_class($footer_links)->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($footer_links)->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 { if ($major_version >= 61) { ok($count == 1, "Found elements with nested list_by_partial:$count"); } else { ok((($count == 1) or ($count == 2)), "Found elements with nested list_by_partial:$count"); } } $count = 0; foreach my $element ($firefox->find_by_class($footer_links)->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 { if ($major_version >= 61) { ok($count == 1, "Found elements with nested find_by_partial:$count"); } else { ok((($count == 1) or ($count == 2)), "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; } if ($major_version >= 61) { ok($count == 1, "Found elements with wantarray find_by_partial:$count"); } else { ok((($count == 1) or ($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; } if ($major_version >= 61) { ok($count == 1, "Found elements with wantarray find_partial:$count"); } else { ok((($count == 1) or ($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('Firefox::Marionette'), "Sent 'Firefox::Marionette' 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 'Firefox::Marionette', "property for '$search_box_id' is now 'Firefox::Marionette'"); } 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, 'Firefox::Marionette'), "Sent 'Firefox::Marionette' to the '$search_box_id' field via the browser"); ok($firefox->clear($element), "Clearing the element via the browser"); ok($firefox->type($element, 'Firefox::Marionette'), "Sent 'Firefox::Marionette' to the '$search_box_id' field via the browser"); last; } my $text = $firefox->find($xpath_for_read_text_and_size)->text(); ok($text, "Read '$text' directly from '$xpath_for_read_text_and_size'"); my $tag_name = $firefox->find($xpath_for_read_text_and_size)->tag_name(); ok($tag_name, "'Lucky' button has a tag name of '$tag_name'"); my $rect; eval { $rect = $firefox->find($xpath_for_read_text_and_size)->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($xpath_for_read_text_and_size)->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('$xpath_for_read_text_and_size')->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($xpath_for_read_text_and_size) ]); 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('$xpath_for_read_text_and_size') ]) returns a hex encoded SHA256 digest"); } $handle = $firefox->selfie(highlights => [ $firefox->find($xpath_for_read_text_and_size) ]); $buffer = undef; $handle->read($buffer, 20); ok($buffer =~ /^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A/smx, "\$firefox->selfie(highlights => [ \$firefox->find('$xpath_for_read_text_and_size') ]) 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('$xpath_for_read_text_and_size') ]) 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) { diag("Clicking on API link with " . $element->uuid()); 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"); 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 (qw( aria_label aria_role clear click is_displayed is_enabled is_selected rect scroll tag_name text type )) { 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"); eval { $firefox->$name(); }; ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->$name() with no parameters produces a Firefox::Marionette::Exception exception"); } $firefox->sleep_time_in_ms(2_000); 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"); ok($firefox->go("https://metacpan.org/pod/Test::More"), "Just directly going to https://metacpan.org/pod/Test::More"); 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 ($is_covering) { } else { $count += 1; } } ok($count == 1, "Downloaded 1 files:$count"); my $deprecated_handle = $firefox->download($download_path); ok($deprecated_handle->isa('GLOB'), "Obtained GLOB from \$firefox->downloaded('$download_path')"); my $handle = $firefox->downloaded($download_path); ok($handle->isa('GLOB'), "Obtained GLOB from \$firefox->downloaded('$download_path')"); 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('keyboard-shortcuts'); 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:$@"); chomp $@; ok($@ =~ /[ ]at[ ]t.01\-marionette[.]t[ ]line[ ]\d+$/smx, "Exception shows source and line numbers"); ok($firefox->release(), "\$firefox->release()"); if ($major_version >= 106) { my $origin = $firefox->find_id($search_box_id); ok($firefox->perform($firefox->wheel($origin, deltaY => 500, origin => 'viewport')), "\$firefox->perform() with a wheel action"); } } 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() || '')); } TODO: { local $TODO = ($ENV{FIREFOX_HOST} || $^O eq 'cygwin' || $^O eq 'MSWin32') ? "\$capabilities->moz_profiles() can contain shorted profile directory names" : undef; 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; } } if (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { diag("Skipping checks that use a file:// url b/c of OpenBSD's unveil functionality - see https://bugzilla.mozilla.org/show_bug.cgi?id=1580271"); 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 { $firefox->install(q[t/addons/not_exists_] . int(rand(50000))); }; chomp $@; ok($@ =~ /Failed[ ]to[ ]find[ ]extension/smx, "\$firefox->install() throws an exception when asked to install a non-existant extension:$@"); 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"); } TODO: { local $TODO = $correct_exit_status == 0 ? q[] : "$version_string is not exiting cleanly"; 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 and extensions"); ($skip_message, $firefox) = start_firefox(0, visible => 0, implicit => 987654, geo => 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 $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 $path = 't/addons/borderify/manifest.json'; if (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { diag("Skipping checks that use a file:// url b/c of OpenBSD's unveil functionality - see https://bugzilla.mozilla.org/show_bug.cgi?id=1580271"); } else { 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; 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"); if ($ENV{FIREFOX_HOST}) { } elsif (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { diag("Skipping checks that use a file:// url b/c of OpenBSD's unveil functionality - see https://bugzilla.mozilla.org/show_bug.cgi?id=1580271"); } else { my $go_path = File::Spec->catfile(Cwd::cwd(), qw(t data iframe.html)); if ($^O eq 'cygwin') { $go_path = $firefox->execute( 'cygpath', '-s', '-m', $go_path ); } $firefox->go("file://$go_path"); my $actual_border; CHECK_BORDER: for my $count ( 1 .. 10 ) { $actual_border = $firefox->script(q{return document.body.style.border}); if ($actual_border =~ /red/smx) { last CHECK_BORDER; } else { sleep 1; } } my $expected_border = "5px solid red"; ok($actual_border eq $expected_border, "Extension is proved to be running correctly: '$actual_border' vs '$expected_border'"); } ok($firefox->uninstall($install_id), "Successfully uninstalled an extension"); } } if ($ENV{FIREFOX_HOST}) { } elsif (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { diag("Skipping checks that use a file:// url b/c of OpenBSD's unveil functionality - see https://bugzilla.mozilla.org/show_bug.cgi?id=1580271"); } elsif ($major_version >= $min_geo_version) { my $new_latitude = -37.123; my $new_longitude = 144.456; $firefox->geo(latitude => $new_latitude, longitude => $new_longitude); my $path = File::Spec->catfile(Cwd::cwd(), qw(t data iframe.html)); if ($^O eq 'cygwin') { $path = $firefox->execute( 'cygpath', '-s', '-m', $path ); } $firefox->go("file://$path"); if (my $geo5 = $firefox->geo()) { my $current_latitude = $geo5->latitude(); my $current_longitude = $geo5->longitude(); ok($current_latitude == $new_latitude, "\$geo5->latitude() has changed after a call to \$firefox->geo(latitude => $new_latitude, longitude => $new_longitude):$current_latitude"); ok($current_longitude == $new_longitude, "\$geo5->longitude() has changed after a call to \$firefox->geo(latitude => $new_latitude, longitude => $new_longitude):$current_longitude"); } elsif (($uname eq 'cygwin') || ($uname eq 'MSWin32')) { diag("Location services may be disabled"); eval { $firefox->dismiss_alert(); }; } } 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 ($^O eq 'cygwin') { diag("\$capabilities->proxy is not supported for " . $^O); skip("\$capabilities->proxy is not supported for " . $^O, 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) { wait_for_server_on($daemon, $daemon->url(), $pid); my $base_url = $daemon->url(); undef $daemon; $firefox->go($base_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($base_url . '?format=txt'); ok($firefox->strip() eq $txt_document, "Correctly retrieved TXT document"); diag($firefox->strip()); if ($major_version >= 61) { my $handle = $firefox->download($base_url . '?format=txt'); my $output = <$handle>; ok($output eq $txt_document, "Correctly downloaded TXT document without timeout"); $handle = $firefox->download($base_url . '?format=txt', 50); $output = <$handle>; ok($output eq $txt_document, "Correctly downloaded TXT document with explicit timeout"); } while(kill 0, $pid) { kill $signals_by_name{TERM}, $pid; sleep 1; waitpid $pid, POSIX::WNOHANG(); } ok($! == POSIX::ESRCH(), "Process $pid no longer exists:$!"); } elsif (defined $pid) { eval 'Devel::Cover::set_coverage("none")' if $is_covering; eval { local $SIG{ALRM} = sub { die "alarm during content server\n" }; alarm 40; $0 = "[Test HTTP Content Server for " . getppid . "]"; diag("Accepting connections for $0"); 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:$@"); } TODO: { local $TODO = $major_version != 60 && $correct_exit_status == 0 ? q[] : "$version_string is not exiting cleanly"; 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, geo => { latitude => 141, longitude => 85, altitude => 40, altitude_accuracy => 20 }); 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 ($^O eq 'cygwin') { diag("\$capabilities->proxy is not supported for " . $^O); skip("\$capabilities->proxy is not supported for " . $^O, 3); } elsif ((exists $Config::Config{'d_fork'}) && (defined $Config::Config{'d_fork'}) && ($Config::Config{'d_fork'} eq 'define')) { if (my $pid = fork) { wait_for_server_on($daemon, $daemon->url(), $pid); my $base_url = $daemon->url(); undef $daemon; $firefox->go($base_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(); } ok($! == POSIX::ESRCH(), "Process $pid no longer exists:$!"); } elsif (defined $pid) { eval 'Devel::Cover::set_coverage("none")' if $is_covering; eval { local $SIG{ALRM} = sub { die "alarm during links and images server\n" }; alarm 40; $0 = "[Test HTTP Links and Images Server for " . getppid . "]"; diag("Accepting connections for $0"); 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"); } } TODO: { local $TODO = $major_version != 60 && $correct_exit_status == 0 ? q[] : "$version_string is not exiting cleanly"; 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(); } sub stringify_scroll_arguments { my (@scroll_arguments) = @_; if (@scroll_arguments) { if (ref $scroll_arguments[0]) { my @attributes; while (my ($key, $value) = each %{$scroll_arguments[0]}) { push @attributes, "$key => '$value'"; } return '{' . (join q[, ], @attributes) . '}'; } else { return $scroll_arguments[0]; } } else { return q[an empty list]; } } sub test_scroll_arguments { my ($number_of_links) = @_; my $number_of_options = 5; if (($number_of_links % $number_of_options) == 0) { return (); } elsif (($number_of_links % $number_of_options) == 1) { return (1); } elsif (($number_of_links % $number_of_options) == 2) { return (0); } elsif (($number_of_links % $number_of_options) == 3) { return ({block => 'end'}); } elsif (($number_of_links % $number_of_options) == 4) { return ({behavior => 'smooth', block => 'end', inline => 'nearest'}); } else { return (); } } 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); } my $proxyPort = empty_port(); diag("Starting new firefox for testing proxy by argument, capabilities, window switching and certificates using proxy port TCP/$proxyPort"); my $proxy_host = 'localhost:' . $proxyPort; if ($major_version == 38) { skip("Skipping b/c proxy must be undefined", 32); } if ($major_version >= 135) { skip("Skipping b/c proxy seems to cause hangs", 32); } ($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", "\$capabilities->proxy()->http() is '$proxy_host'"); ok($capabilities->proxy()->https() eq "$proxy_host", "\$capabilities->proxy()->https() is '$proxy_host'"); } 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); } elsif ($ENV{FIREFOX_NO_VISIBLE}) { diag("\$capabilities->headless is forced on for FIREFOX_NO_VISIBLE testing"); skip("\$capabilities->headless is forced on for FIREFOX_NO_VISIBLE 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"); if ($major_version >= 121) { my $initial_url = $firefox->script("return window.location.href"); my $script_window = $firefox->script("return window"); ok($script_window eq $new_window, "\$firefox->script(\"return window\") matches \$firefox->new_window() output"); my $argument_url = $firefox->script("return arguments[0].location.href", args => [ $script_window ]); ok($argument_url eq $initial_url, "window object can be used as an in and out parameter for javascript calls:$argument_url:$initial_url"); } 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 $certificate = Firefox::Marionette::Certificate->new(); ok(!$certificate->is_server_cert(), "Firefox::Marionette::Certificate->new() does not produce a server cert (test coverage)"); 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); } my $count = 0; foreach my $certificate (sort { display_name($a) cmp display_name($b) } $firefox->certificates()) { if ($firefox->is_trusted($certificate)) { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is trusted in the current profile"); } else { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " is NOT trusted in the current profile"); } } 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"); } $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())); if ($major_version > 52) { if (defined $certificate->nickname()) { ok($certificate->nickname(), Encode::encode('UTF-8', display_name($certificate)) . " has a nickname of " . $certificate->nickname()); } else { ok(1, Encode::encode('UTF-8', display_name($certificate)) . " does not have a specified nickname"); } } 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)) . ' does not have a 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; if (!$ENV{RELEASE_TESTING}) { last; } } if ($ENV{RELEASE_TESTING}) { ok($count > 0, "There are $count certificates in the firefox database"); } } TODO: { local $TODO = $correct_exit_status == 0 ? q[] : "$version_string is not exiting cleanly"; 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; } my $maximise; SKIP: { diag("Starting new firefox for testing \%ENV proxy, min/maxing and killing firefox"); local %ENV = %ENV; my $proxyHttpPort = empty_port(); my $proxyHttpsPort = empty_port(); my $proxyFtpPort = empty_port(); if ($major_version == 38) { } else { $ENV{http_proxy} = 'http://localhost:' . $proxyHttpPort; $ENV{https_proxy} = 'http://localhost:' . $proxyHttpsPort; $ENV{ftp_proxy} = 'ftp://localhost:' . $proxyFtpPort; } if ($major_version >= 135) { skip("Skipping b/c proxy seems to cause hangs", 15); } ($skip_message, $firefox) = start_firefox(1, addons => 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 = $uname eq 'linux' ? '' : "Initial width/height parameters not entirely stable in $uname"; 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 $uname 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"); if ($ENV{FIREFOX_HOST}) { diag("\$capabilities->headless is forced on for FIREFOX_HOST testing"); } elsif ($ENV{FIREFOX_NO_VISIBLE}) { diag("\$capabilities->headless is forced on for FIREFOX_NO_VISIBLE testing"); } else { ok(!$capabilities->moz_headless(), "\$capabilities->moz_headless() is set to false"); } diag("Final Browser version is " . $capabilities->browser_version()); if ($major_version >= 51) { SKIP: { my $webgl2 = $firefox->script(q[return document.createElement('canvas').getContext('webgl2') ? true : false;]); my $experimental = $firefox->script(q[return document.createElement('canvas').getContext('experimental-webgl') ? true : false;]); my $other = $firefox->script(q[return ("WebGLRenderingContext" in window) ? true : false;]); my $webgl_ok = 1; if ($webgl2) { diag("WebGL (webgl2) is working correctly for " . $capabilities->browser_version() . " on $uname"); } elsif ($experimental) { diag("WebGL (experimental) is working correctly for " . $capabilities->browser_version() . " on $uname"); } elsif ($other) { diag("WebGL (WebGLRenderingContext) is providing some sort of support for " . $capabilities->browser_version() . " on $uname"); } elsif (($^O eq 'cygwin') || ($^O eq 'darwin') || ($^O eq 'MSWin32')) { $webgl_ok = 0; diag("WebGL is NOT working correctly for " . $capabilities->browser_version() . " on $uname"); } else { my $glxinfo = `glxinfo 2>&1`; $glxinfo =~ s/\s+/ /smxg; if ($? == 0) { if ($glxinfo =~ /^Error:/smx) { diag("WebGL is NOT working correctly for " . $capabilities->browser_version() . " on $uname, probably because glxinfo has failed:$glxinfo"); } else { $webgl_ok = 0; diag("WebGL is NOT working correctly for " . $capabilities->browser_version() . " on $uname but glxinfo has run successfully:$glxinfo"); } } else { $webgl_ok = 0; diag("WebGL is NOT working correctly for " . $capabilities->browser_version() . " on $uname and glxinfo cannot be run:$?"); } } ok($webgl_ok, "WebGL is enabled when visible and addons are turned on"); } } 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:' . $proxyHttpPort, "\$capabilities->proxy()->http() is 'localhost:$proxyHttpPort':" . $capabilities->proxy()->http()); ok($capabilities->proxy()->https() eq 'localhost:' . $proxyHttpsPort, "\$capabilities->proxy()->https() is 'localhost:$proxyHttpsPort'"); if ($major_version < 90) { ok($capabilities->proxy()->ftp() eq 'localhost:' . $proxyFtpPort, "\$capabilities->proxy()->ftp() is 'localhost:$proxyFtpPort'"); } } SKIP: { 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()"); 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); } TODO: { local $TODO = $correct_exit_status == 0 ? q[] : $capabilities->browser_version() . " is not exiting cleanly"; 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); } TODO: { local $TODO = $correct_exit_status == 0 ? q[] : $capabilities->browser_version() . " is not exiting cleanly"; 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); } TODO: { local $TODO = $correct_exit_status == 0 ? q[] : $capabilities->browser_version() . " is not exiting cleanly"; 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: { diag("Starting new firefox for testing visibility and TLS proxy servers"); my $proxyPort = empty_port(); my $proxy_host = 'localhost:' . $proxyPort; if ($major_version == 45) { skip("Skipping b/c of proxy setCharPref exceptions", 1); } if ($major_version == 38) { skip("Skipping b/c proxy must be undefined", 1); } if ($major_version >= 135) { skip("Skipping b/c proxy seems to cause hangs", 1); } ($skip_message, $firefox) = start_firefox(1, visible => 1, width => 800, height => 600,capabilities => Firefox::Marionette::Capabilities->new(moz_headless => 0, proxy => Firefox::Marionette::Proxy->new(tls => $proxy_host))); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 451); } ok($firefox, "Firefox has started in Marionette mode with visible set to 1"); 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 'pac', "\$capabilities->proxy()->type() is 'pac'"); ok($capabilities->proxy()->pac() =~ /^data:text\/plain,function(?:[ ]|%20)FindProxyForURL[(][)](?:[{]|%7B)return(?:[ ]|%20)(?:"|%22)HTTPS(?:[ ]|%20)localhost:$proxyPort(?:"|%22)(?:[}]|%7D)$/smx, qq[\$capabilities->proxy()->pac() is 'data:text/plain,function FindProxyForURL(){return "HTTPS localhost:$proxyPort"}':] . $capabilities->proxy()->pac()); } if ($major_version < 52) { diag("Not attempting to resize for Firefox $major_version"); } elsif ($maximise) { local $TODO = q[]; my $count = 0; my $resize_works; foreach my $display ($firefox->displays()) { $count += 1; ok(defined $display->usage(), "\$display->usage() is defined:" . $display->usage()); ok(defined $display->designation(), "\$display->designation() is defined:" . $display->designation()); ok($display->sar() =~ /^\d+(?:[.]\d+)?:\d+$/smx, "\$display->sar() is a ratio:" . $display->sar()); ok($display->dar() =~ /^\d+(?:[.]\d+)?(?::\d+)?$/smx, "\$display->dar() is a ratio or a floating point number:" . $display->dar()); ok($display->par() =~ /^\d+(?:[.]\d+)?(?::\d+(?:[.]\d+)?)?$/smx, "\$display->par() is a ratio or a floating point number:" . $display->par()); my $result; eval { $result = $firefox->resize($display->width(), $display->height()); $resize_works = 1; } or do { if ($major_version < 60) { chomp $@; diag("Failed to resize browser for old browser version $major_version:$@"); } else { ok(0, "Failed to resize browser for a modern browser:$@"); diag("Failed to resize browser for a modern browser:$@"); } }; if ($result) { ok(1, "Resized the display to " . $display->width . "x" . $display->height()); last unless ($ENV{RELEASE_TESTING}); } else { ok(1, "Not able to resize the display to " . $display->width . "x" . $display->height()); } } ok($count, "$count displays are currently known to firefox"); my $iphone_count = 0; foreach my $display ($firefox->displays(qr/iphone/smxi)) { $iphone_count += 1; ok($display->usage() =~ /iphone/smxi, "iPhone display detected:" . $display->usage()); } ok($iphone_count, "$iphone_count displays are for an iphone"); ok($firefox->displays(qr/iphone/i) < $firefox->displays(), "There are fewer displays for iphones than all displays"); if ($ENV{FIREFOX_HOST}) { } elsif (($^O eq 'openbsd') && (Cwd::cwd() !~ /^($quoted_home_directory\/Downloads|\/tmp)/)) { diag("Skipping checks that use a file:// url b/c of OpenBSD's unveil functionality - see https://bugzilla.mozilla.org/show_bug.cgi?id=1580271"); } elsif ($major_version >= $min_geo_version) { my $path = File::Spec->catfile(Cwd::cwd(), qw(t data visible.html)); if ($^O eq 'cygwin') { $path = $firefox->execute( 'cygpath', '-s', '-m', $path ); } my $url = "file://$path"; ok($firefox->go($url), "$url has been loaded"); my $element = $firefox->find_id('username'); if (($resize_works) && ($firefox->resize(800, 600))) { my $percentage = $firefox->percentage_visible($element); ok($percentage == 0, "Percentage visible is 0% for the username field:$percentage"); if ($major_version >= 59) { ok($firefox->scroll($element, { block => 'center' }), "Scroll until the username field is in the center of the screen"); $percentage = $firefox->percentage_visible($element); ok($percentage > 90, "Percentage visible is greater than 90% for the username field:$percentage"); # should be 100% but weird things happen apparently } } else { diag("Skipping checks that require resize to work"); } } } TODO: { local $TODO = $correct_exit_status == 0 ? q[] : "$version_string is not exiting cleanly"; 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 shortcut TLS proxy servers"); my $proxyPort = empty_port(); my $proxy_host = 'localhost:' . $proxyPort; if (($major_version == 45) || ($major_version == 38)) { skip("Skipping b/c of segmentation faults for proxy capabilities", 5); } if (($uname eq 'cygwin') || ($uname eq 'MSWin32')) { skip("Skipping b/c of hangs in Windows 11 with recent firefox (>= 135) for proxy capabilities", 5); } if ($major_version >= 135) { skip("Skipping b/c proxy seems to cause hangs", 5); } ($skip_message, $firefox) = start_firefox(0, capabilities => Firefox::Marionette::Capabilities->new(moz_headless => 0, page_load_strategy => 'none', proxy => Firefox::Marionette::Proxy->new(host => $proxy_host)), proxy => "https://$proxy_host"); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 5); } 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"); 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 (!$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 'pac', "\$capabilities->proxy()->type() is 'pac'"); ok($capabilities->proxy()->pac() =~ /^data:text\/plain,function(?:[ ]|%20)FindProxyForURL[(][)](?:[{]|%7B)return(?:[ ]|%20)(?:"|%22)HTTPS(?:[ ]|%20)$proxy_host(?:"|%22)(?:[}]|%7D)$/smx, qq[\$capabilities->proxy()->pac() is 'data:text/plain,function FindProxyForURL(){return "HTTPS $proxy_host"}':] . $capabilities->proxy()->pac()); } TODO: { local $TODO = $correct_exit_status == 0 ? q[] : "$version_string is not exiting cleanly"; 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 shortcut normal proxy servers"); my $proxyPort = empty_port(); my $proxy_host = 'localhost:' . $proxyPort; if ($major_version == 38) { skip("Skipping b/c of segmentation faults for proxy capabilities", 5); } if (($uname eq 'cygwin') || ($uname eq 'MSWin32')) { skip("Skipping b/c of hangs in Windows 11 with recent firefox (>= 135) for proxy capabilities", 5); } if ($major_version >= 135) { skip("Skipping b/c proxy seems to cause hangs", 5); } ($skip_message, $firefox) = start_firefox(0, proxy => URI::URL->new("http://$proxy_host")); if (!$skip_message) { $at_least_one_success = 1; } if ($skip_message) { skip($skip_message, 5); } 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"); 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, "\$capabilities->proxy()->http() is '$proxy_host':" . $capabilities->proxy()->http()); ok($capabilities->proxy()->https() eq $proxy_host, "\$capabilities->proxy()->https() is '$proxy_host'"); } TODO: { local $TODO = $correct_exit_status == 0 ? q[] : "$version_string is not exiting cleanly"; ok($firefox->quit() == $correct_exit_status, "Firefox has closed with an exit status of $correct_exit_status:" . $firefox->child_error()); } } 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 $argument_string = q[]; if ($ENV{FIREFOX_VISIBLE}) { $argument_string = q[visible => 1]; } my $exit_status = system { $^X } $^X, (map { "-I$_" } @INC), '-MFirefox::Marionette', '-e', "my \$f = Firefox::Marionette->new($argument_string); 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($argument_string); \$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 $via = $ENV{FIREFOX_VIA} ? q[, via => "] . $ENV{FIREFOX_VIA} . q["] : q[]; 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["] . $via . ($argument_string ? ", $argument_string" : q[]) . q[); $fh->print($f->ssh_local_directory()) or die "PRINT:$!"; close($fh) or die "CLOSE:$!";']; $command =~ s/([@])/\\$1/smxg; 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', qq['\$f = Firefox::Marionette->new($argument_string); print \$f->root_directory();']; my $directory = `$command`; ok(!-d $directory, "Firefox::Marionette->new() cleans up the local directory at $directory"); } } } ok($profiles_work, "Specified profile names work"); 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"); my $total_run_time = time - $^T; if (defined $alarm) { my $remaining_time = ($alarm - $total_run_time); diag("Total runtime is " . $total_run_time . " seconds (remaining time before alarm of $alarm is $remaining_time)"); } else { diag("Total runtime is " . $total_run_time . " seconds"); } done_testing(); Firefox-Marionette-1.63/README.md0000644000175000017500000047353714763400566015107 0ustar davedave# NAME Firefox::Marionette - Automate the Firefox browser with the Marionette protocol # VERSION Version 1.63 # 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('page-content')->find_id('metacpan_search-input')->type('Test::More'); say "Height of page-content div is " . $firefox->find_class('page-content')->css('height'); my $file_handle = $firefox->selfie(); $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click(); $firefox->find_partial('Download')->click(); # DESCRIPTION This is a client module to automate the Mozilla Firefox browser via the [Marionette protocol](https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/Protocol) # CONSTANTS ## BCD\_PATH returns the local path used for storing the brower compability data for the [agent](#agent) method when the `stealth` parameter is supplied to the [new](#new) method. This database is built by the [build-bcd-for-firefox](https://metacpan.org/pod/build-bcd-for-firefox) binary. # 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\_bookmark accepts a [bookmark](https://metacpan.org/pod/Firefox::Marionette::Bookmark) as a parameter and adds the specified bookmark to the Firefox places database. use Firefox::Marionette(); my $bookmark = Firefox::Marionette::Bookmark->new( url => 'https://metacpan.org', title => 'This is MetaCPAN!' ); my $firefox = Firefox::Marionette->new()->add_bookmark($bookmark); This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## 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](https://datatracker.ietf.org/doc/html/rfc7468#section-5). - string - a string containing a single [PEM encoded X.509 certificate](https://datatracker.ietf.org/doc/html/rfc7468#section-5) - trust - This is the [trustargs](https://www.mankier.com/1/certutil#-t) value for [NSS](https://wiki.mozilla.org/NSS). If defaults to 'C,,'; This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) 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](https://metacpan.org/pod/Firefox::Marionette::Cookie) object as the first parameter and adds it to the current cookie jar. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) 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](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors/InvalidCookieDomain). ## 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](#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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/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](https://metacpan.org/pod/Firefox::Marionette) 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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-action). Forms with no [action attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-action) 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](https://datatracker.ietf.org/doc/html/rfc2617). 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](https://metacpan.org/pod/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 my $form_login = Firefox::Marionette::Login(host => 'https://github.com', user => 'me2@example.org', password => 'uiop[]', user_field => 'login', password_field => 'password'); $firefox->add_login($form_login); # or just directly $firefox->add_login(host => 'https://github.com', user => 'me2@example.org', password => 'uiop[]', user_field => 'login', password_field => 'password'); Note for HTTP Authentication, the [realm](https://datatracker.ietf.org/doc/html/rfc2617#section-2) must perfectly match the correct [realm](https://datatracker.ietf.org/doc/html/rfc2617#section-2) supplied by the server. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) 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](#delete_site_header) method ## add\_webauthn\_authenticator accepts a hash of the following keys; - has\_resident\_key - boolean value to indicate if the authenticator will support [client side discoverable credentials](https://www.w3.org/TR/webauthn-2/#client-side-discoverable-credential) - has\_user\_verification - boolean value to determine if the [authenticator](https://www.w3.org/TR/webauthn-2/#virtual-authenticators) supports [user verification](https://www.w3.org/TR/webauthn-2/#user-verification). - is\_user\_consenting - boolean value to determine the result of all [user consent](https://www.w3.org/TR/webauthn-2/#user-consent) [authorization gestures](https://www.w3.org/TR/webauthn-2/#authorization-gesture), and by extension, any [test of user presence](https://www.w3.org/TR/webauthn-2/#test-of-user-presence) performed on the [Virtual Authenticator](https://www.w3.org/TR/webauthn-2/#virtual-authenticators). If set to true, a [user consent](https://www.w3.org/TR/webauthn-2/#user-consent) will always be granted. If set to false, it will not be granted. - is\_user\_verified - boolean value to determine the result of [User Verification](https://www.w3.org/TR/webauthn-2/#user-verification) performed on the [Virtual Authenticator](https://www.w3.org/TR/webauthn-2/#virtual-authenticators). If set to true, [User Verification](https://www.w3.org/TR/webauthn-2/#user-verification) will always succeed. If set to false, it will fail. - protocol - the [protocol](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#protocol) spoken by the authenticator. This may be [CTAP1\_U2F](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#CTAP1_U2F), [CTAP2](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#CTAP2) or [CTAP2\_1](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#CTAP2_1). - transport - the [transport](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#transport) simulated by the authenticator. This may be [BLE](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#BLE), [HYBRID](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#HYBRID), [INTERNAL](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#INTERNAL), [NFC](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#NFC), [SMART\_CARD](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#SMART_CARD) or [USB](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#USB). It returns the newly created [authenticator](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator). use Firefox::Marionette(); use Crypt::URandom(); my $user_name = MIME::Base64::encode_base64( Crypt::URandom::urandom( 10 ), q[] ) . q[@example.com]; my $firefox = Firefox::Marionette->new( webauthn => 0 ); my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() ); $firefox->go('https://webauthn.io'); $firefox->find_id('input-email')->type($user_name); $firefox->find_id('register-button')->click(); $firefox->await(sub { sleep 1; $firefox->find_class('alert-success'); }); $firefox->find_id('login-button')->click(); $firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); }); ## add\_webauthn\_credential accepts a hash of the following keys; - authenticator - contains the [authenticator](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator) that the credential will be added to. If this parameter is not supplied, the credential will be added to the default authenticator, if one exists. - host - contains the domain that this credential is to be used for. In the language of [WebAuthn](https://www.w3.org/TR/webauthn-2), this field is referred to as the [relying party identifier](https://www.w3.org/TR/webauthn-2/#relying-party-identifier) or [RP ID](https://www.w3.org/TR/webauthn-2/#rp-id). - id - contains the unique id for this credential, also known as the [Credential ID](https://www.w3.org/TR/webauthn-2/#credential-id). If this is not supplied, one will be generated. - is\_resident - contains a boolean that if set to true, a [client-side discoverable credential](https://w3c.github.io/webauthn/#client-side-discoverable-credential) is created. If set to false, a [server-side credential](https://w3c.github.io/webauthn/#server-side-credential) is created instead. - private\_key - either a [RFC5958](https://www.rfc-editor.org/rfc/rfc5958) encoded private key encoded using [encode\_base64url](https://metacpan.org/pod/MIME::Base64::encode_base64url) or a hash containing the following keys; - name - contains the name of the private key algorithm, such as "RSA-PSS" (the default), "RSASSA-PKCS1-v1\_5", "ECDSA" or "ECDH". - size - contains the modulus length of the private key. This is only valid for "RSA-PSS" or "RSASSA-PKCS1-v1\_5" private keys. - hash - contains the name of the hash algorithm, such as "SHA-512" (the default). This is only valid for "RSA-PSS" or "RSASSA-PKCS1-v1\_5" private keys. - curve - contains the name of the curve for the private key, such as "P-384" (the default). This is only valid for "ECDSA" or "ECDH" private keys. - sign\_count - contains the initial value for a [signature counter](https://w3c.github.io/webauthn/#signature-counter) associated to the [public key credential source](https://w3c.github.io/webauthn/#public-key-credential-source). It will default to 0 (zero). - user - contains the [userHandle](https://w3c.github.io/webauthn/#public-key-credential-source-userhandle) associated to the credential encoded using [encode\_base64url](https://metacpan.org/pod/MIME::Base64::encode_base64url). This property is optional. It returns the newly created [credential](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Credential). If of course, the credential is just created, it probably won't be much good by itself. However, you can use it to recreate a credential, so long as you know all the parameters. use Firefox::Marionette(); use Crypt::URandom(); my $user_name = MIME::Base64::encode_base64( Crypt::URandom::urandom( 10 ), q[] ) . q[@example.com]; my $firefox = Firefox::Marionette->new(); $firefox->go('https://webauthn.io'); $firefox->find_id('input-email')->type($user_name); $firefox->find_id('register-button')->click(); $firefox->await(sub { sleep 1; $firefox->find_class('alert-success'); }); $firefox->find_id('login-button')->click(); $firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); }); foreach my $credential ($firefox->webauthn_credentials()) { $firefox->delete_webauthn_credential($credential); \# ... time passes ... $firefox->add_webauthn_credential( id => $credential->id(), host => $credential->host(), user => $credential->user(), private_key => $credential->private_key(), is_resident => $credential->is_resident(), sign_count => $credential->sign_count(), ); } $firefox->go('about:blank'); $firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_COOKIES()); $firefox->go('https://webauthn.io'); $firefox->find_id('input-email')->type($user_name); $firefox->find_id('login-button')->click(); $firefox->await(sub { sleep 1; $firefox->find_class('hero confetti'); }); ## 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](http://kb.mozillazine.org/Command_line_arguments#List_of_command_line_arguments_.28incomplete.29) cannot be automated. ## agent accepts an optional value for the [User-Agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) header and sets this using the profile preferences and inserting [javascript](#script) into the current page. It returns the current value, such as 'Mozilla/5.0 (<system-information>) <platform> (<platform-details>) <extensions>'. This value is retrieved with [navigator.userAgent](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgent). This method can be used to set a user agent string like so; use Firefox::Marionette(); use strict; # useragents.me should only be queried once a month or less. # these UA strings should be cached locally. my %user_agent_strings = map { $_->{ua} => $_->{pct} } @{$firefox->json("https://www.useragents.me/api")->{data}}; my ($user_agent) = reverse sort { $user_agent_strings{$a} <=> $user_agent_strings{$b} } keys %user_agent_strings; my $firefox = Firefox::Marionette->new(); $firefox->agent($user_agent); # agent is now the most popular agent from useragents.me If the user agent string that is passed as a parameter looks like a [Chrome](https://www.google.com/chrome/), [Edge](https://microsoft.com/edge) or [Safari](https://www.apple.com/safari/) user agent string, then this method will also try and change other profile preferences to match the new agent string. These parameters are; - general.appversion.override - general.oscpu.override - general.platform.override - network.http.accept - network.http.accept-encoding - network.http.accept-encoding.secure - privacy.donottrackheader.enabled In addition, this method will accept a hash of values as parameters as well. When a hash is provided, this method will alter specific parts of the normal Firefox User Agent. These hash parameters are; - os - The desired operating system, known values are "linux", "win32", "darwin", "freebsd", "netbsd", "openbsd" and "dragonfly" - version - A specific version of firefox, such as 120. - arch - A specific version of the architecture, such as "x86\_64" or "aarch64" or "s390x". - increment - A specific offset from the actual version of firefox, such as -5 These parameters can be used to set a user agent string like so; use Firefox::Marionette(); use strict; my $firefox = Firefox::Marionette->new(); $firefox->agent(os => 'freebsd', version => 118); # user agent is now equal to # Mozilla/5.0 (X11; FreeBSD amd64; rv:109.0) Gecko/20100101 Firefox/118.0 $firefox->agent(os => 'linux', arch => 's390x', version => 115); # user agent is now equal to # Mozilla/5.0 (X11; Linux s390x; rv:109.0) Gecko/20100101 Firefox/115.0 If the `stealth` parameter has supplied to the [new](#new) method, it will also attempt to create known specific javascript functions to imitate the required browser. If the database built by [build-bcd-for-firefox](https://metacpan.org/pod/build-bcd-for-firefox) is accessible, then it will also attempt to delete/provide dummy implementations for the corresponding [javascript attributes](https://github.com/mdn/browser-compat-data) for the desired browser. The following websites have been very useful in testing these ideas; - [https://browserleaks.com/javascript](https://browserleaks.com/javascript) - [https://www.amiunique.org/fingerprint](https://www.amiunique.org/fingerprint) - [https://bot.sannysoft.com/](https://bot.sannysoft.com/) - [https://lraj22.github.io/browserfeatcl/](https://lraj22.github.io/browserfeatcl/) Importantly, this will break [feature detection](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Feature_detection) for any website that relies on it. See [IMITATING OTHER BROWSERS](#imitating-other-browsers) a discussion of these types of techniques. These changes are not foolproof, but it is interesting to see what can be done with modern browsers. All this behaviour should be regarded as extremely experimental and subject to change. Feedback welcome. ## 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'. ## arch returns the architecture of the machine running firefox. Should be something like 'x86\_64' or 'arm'. This is only intended for test suite support. ## aria\_label accepts an [element](https://metacpan.org/pod/Firefox::Marionette::Element) as the parameter. It returns the [ARIA label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label) for the [element](https://metacpan.org/pod/Firefox::Marionette::Element). ## aria\_role accepts an [element](https://metacpan.org/pod/Firefox::Marionette::Element) as the parameter. It returns the [ARIA role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles) for the [element](https://metacpan.org/pod/Firefox::Marionette::Element). ## async\_script accepts a scalar containing a javascript function that is executed in the browser. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. The executing javascript is subject to the [script](https://metacpan.org/pod/Firefox::Marionette::Timeouts#script) timeout, which, by default is 30 seconds. ## attribute accepts an [element](https://metacpan.org/pod/Firefox::Marionette::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 from the HTML source code, the [property](#property) method will return the current content. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $element = $firefox->find_id('metacpan_search-input'); !defined $element->attribute('value') or die "attribute is defined but did not exist in the html source!"; $element->type('Test::More'); !defined $element->attribute('value') or die "attribute has changed but only the property should have changed!"; ## await accepts a subroutine reference as a parameter and then executes the subroutine. If a [not found](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception is thrown, this method will sleep for [sleep\_time\_in\_ms](#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->await(sub { $firefox->find_class('autocomplete-suggestion'); })->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](https://metacpan.org/pod/Firefox::Marionette::Timeouts#page_load) duration to elapse before returning, which, by default is 5 minutes. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) 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. ## download accepts a [URI](https://metacpan.org/pod/URI) and an optional timeout in seconds (the default is 5 minutes) as parameters and downloads the [URI](https://metacpan.org/pod/URI) in the background and returns a handle to the downloaded file. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new(); my $handle = $firefox->download('https://raw.githubusercontent.com/david-dick/firefox-marionette/master/t/data/keepassxs.csv'); foreach my $line (<$handle>) { print $line; } ## bookmarks accepts either a scalar or a hash as a parameter. The scalar may by the title of a bookmark or the [URL](https://metacpan.org/pod/URI::URL) of the bookmark. The hash may have the following keys; - title - The title of the bookmark. - url - The url of the bookmark. returns a list of all [Firefox::Marionette::Bookmark](https://metacpan.org/pod/Firefox::Marionette::Bookmark) objects that match the supplied parameters (if any). use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new(); foreach my $bookmark ($firefox->bookmarks(title => 'This is MetaCPAN!')) { say "Bookmark found"; } # OR foreach my $bookmark ($firefox->bookmarks()) { say "Bookmark found with URL " . $bookmark->url(); } # OR foreach my $bookmark ($firefox->bookmarks('https://metacpan.org')) { say "Bookmark found"; } ## 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](#sleep_time_in_ms) milliseconds and then execute the subroutine again. When a [not found](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception is thrown, this method will return [itself](https://metacpan.org/pod/Firefox::Marionette) 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->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click(); $firefox->bye(sub { $firefox->find_name('metacpan_search-input') })->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click(); ## cache\_keys returns the set of all cache keys from [Firefox::Marionette::Cache](https://metacpan.org/pod/Firefox::Marionette::Cache). use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(); foreach my $key_name ($firefox->cache_keys()) { my $key_value = $firefox->check_cache_key($key_name); if (Firefox::Marionette::Cache->$key_name() != $key_value) { warn "This module this the value of $key_name is " . Firefox::Marionette::Cache->$key_name(); warn "Firefox thinks the value of $key_name is $key_value"; } } ## capabilities returns the [capabilities](https://metacpan.org/pod/Firefox::Marionette::Capabilities) of the current firefox binary. You can retrieve [timeouts](https://metacpan.org/pod/Firefox::Marionette::Timeouts) or a [proxy](https://metacpan.org/pod/Firefox::Marionette::Proxy) with this method. ## certificate\_as\_pem accepts a [certificate stored in the Firefox database](https://metacpan.org/pod/Firefox::Marionette::Certificate) as a parameter and returns a [PEM encoded X.509 certificate](https://datatracker.ietf.org/doc/html/rfc7468#section-5) 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](https://metacpan.org/pod/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](https://metacpan.org/pod/Firefox::Marionette::Certificate). 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"; print 'PEM Encoded Certificate ' . "\n" . $firefox->certificate_as_pem($certificate) . "\n"; } This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## check\_cache\_key accepts a [cache\_key](https://metacpan.org/pod/Firefox::Marionette::Cache) as a parameter. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(); foreach my $key_name ($firefox->cache_keys()) { my $key_value = $firefox->check_cache_key($key_name); if (Firefox::Marionette::Cache->$key_name() != $key_value) { warn "This module this the value of $key_name is " . Firefox::Marionette::Cache->$key_name(); warn "Firefox thinks the value of $key_name is $key_value"; } } This method returns the [cache\_key](https://metacpan.org/pod/Firefox::Marionette::Cache)'s actual value from firefox as a number. This may differ from the current value of the key from [Firefox::Marionette::Cache](https://metacpan.org/pod/Firefox::Marionette::Cache) as these values have changed as firefox has evolved. ## 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](#context) method for an alternative methods for changing the context. ## chrome\_window\_handle returns a [server-assigned identifier for the current chrome window that uniquely identifies it](https://metacpan.org/pod/Firefox::Marionette::WebWindow) 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](#window_handle) and appropriate [context](#context) calls for [Firefox 94 and after](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/94#webdriver_conformance_marionette). ## chrome\_window\_handles returns [identifiers](https://metacpan.org/pod/Firefox::Marionette::WebWindow) 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](#window_handles) and appropriate [context](#context) calls for [Firefox 94 and after](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/94#webdriver_conformance_marionette). ## clear accepts a [element](https://metacpan.org/pod/Firefox::Marionette::Element) as the first parameter and clears any user supplied input ## clear\_cache accepts a single flag parameter, which can be an ORed set of keys from [Firefox::Marionette::Cache](https://metacpan.org/pod/Firefox::Marionette::Cache) and clears the appropriate sections of the cache. If no flags parameter is supplied, the default is [CLEAR\_ALL](https://metacpan.org/pod/Firefox::Marionette::Cache#CLEAR_ALL). Note that this method, unlike [delete\_cookies](#delete_cookies) will actually delete all cookies for all hosts, not just the current webpage. use Firefox::Marionette(); use Firefox::Marionette::Cache qw(:all); my $firefox = Firefox::Marionette->new()->go('https://do.lots.of.evil/')->clear_cache(); # default clear all $firefox->go('https://cookies.r.us')->clear_cache(CLEAR_COOKIES()); This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## clear\_pref accepts a [preference](http://kb.mozillazine.org/About:config) name and restores it to the original value. See the [get\_pref](#get_pref) and [set\_pref](#set_pref) methods to get a preference value and to set to it to a particular value. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(); $firefox->clear_pref('browser.search.defaultenginename'); ## click accepts a [element](https://metacpan.org/pod/Firefox::Marionette::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](https://metacpan.org/pod/Firefox::Marionette::Timeouts#page_load) duration to elapse before returning, which, by default is 5 minutes. The [click](#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](https://metacpan.org/pod/Firefox::Marionette::WebWindow). You will need to [switch\_to\_window](#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](https://metacpan.org/pod/Firefox::Marionette::WebWindow). ## 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](#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](#content) and [chrome](#chrome) methods for alternative methods for changing the context. ## cookies returns the [contents](https://metacpan.org/pod/Firefox::Marionette::Cookie) 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](https://metacpan.org/pod/Firefox::Marionette::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](#chrome_window_handle). ## delete\_bookmark accepts a [bookmark](https://metacpan.org/pod/Firefox::Marionette::Bookmark) as a parameter and deletes the bookmark from the Firefox database. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new(); foreach my $bookmark (reverse $firefox->bookmarks()) { if ($bookmark->parent_guid() ne Firefox::Marionette::Bookmark::ROOT()) { $firefox->delete_bookmark($bookmark); } } say "Bookmarks? We don't need no stinking bookmarks!"; This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## delete\_certificate accepts a [certificate stored in the Firefox database](https://metacpan.org/pod/Firefox::Marionette::Certificate) 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](https://metacpan.org/pod/Firefox::Marionette) 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](https://metacpan.org/pod/Firefox::Marionette) 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! Note that this method will only delete cookies for the current site. See [clear\_cache](#clear_cache) for an alternative. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) 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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent), [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) and [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) headers from all future requests This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## delete\_login accepts a [login](https://metacpan.org/pod/Firefox::Marionette::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](https://metacpan.org/pod/Firefox::Marionette) 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](https://metacpan.org/pod/Firefox::Marionette) 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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent), [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) and [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) headers from all future requests to metacpan.org. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## delete\_webauthn\_all\_credentials This method accepts an optional [authenticator](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator), in which case it will delete all [credentials](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Credential) from this authenticator. If no parameter is supplied, the default authenticator will have all credentials deleted. my $firefox = Firefox::Marionette->new(); my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() ); $firefox->delete_webauthn_all_credentials($authenticator); $firefox->delete_webauthn_all_credentials(); ## delete\_webauthn\_authenticator This method accepts an optional [authenticator](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator), in which case it will delete this authenticator from the current Firefox instance. If no parameter is supplied, the default authenticator will be deleted. my $firefox = Firefox::Marionette->new(); my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() ); $firefox->delete_webauthn_authenticator($authenticator); $firefox->delete_webauthn_authenticator(); ## delete\_webauthn\_credential This method accepts either a [credential](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Credential) and an [authenticator](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator), in which case it will remove the credential from the supplied authenticator or use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(); my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() ); foreach my $credential ($firefox->webauthn_credentials($authenticator)) { $firefox->delete_webauthn_credential($credential, $authenticator); } just a [credential](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Credential), in which case it will remove the credential from the default authenticator. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(); ... foreach my $credential ($firefox->webauthn_credentials()) { $firefox->delete_webauthn_credential($credential); } This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## developer returns true if the [current version](#browser_version) of firefox is a [developer edition](https://www.mozilla.org/en-US/firefox/developer/) (does the minor version number end with an 'b\\d+'?) version. ## dismiss\_alert dismisses a currently displayed modal message box ## displays accepts an optional regex to filter against the [usage for the display](https://metacpan.org/pod/Firefox::Marionette::Display#usage) and returns a list of all the [known displays](https://en.wikipedia.org/wiki/List_of_common_resolutions) as a [Firefox::Marionette::Display](https://metacpan.org/pod/Firefox::Marionette::Display). use Firefox::Marionette(); use Encode(); use v5.10; my $firefox = Firefox::Marionette->new( visible => 1, kiosk => 1 )->go('http://metacpan.org');; my $element = $firefox->find_id('metacpan_search-input'); foreach my $display ($firefox->displays(qr/iphone/smxi)) { say 'Can Firefox resize for "' . Encode::encode('UTF-8', $display->usage(), 1) . '"?'; if ($firefox->resize($display->width(), $display->height())) { say 'Now displaying with a Pixel aspect ratio of ' . $display->par(); say 'Now displaying with a Storage aspect ratio of ' . $display->sar(); say 'Now displaying with a Display aspect ratio of ' . $display->dar(); } else { say 'Apparently NOT!'; } } ## downloaded 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('page-content')->find_id('metacpan_search-input')->type('Test::More'); $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click(); $firefox->find_partial('Download')->click(); while(!$firefox->downloads()) { sleep 1 } foreach my $path ($firefox->downloads()) { my $handle = $firefox->downloaded($path); # do something with downloaded file handle } ## downloading returns true if any files in [downloads](#downloads) end in `.part` use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); $firefox->find_class('page-content')->find_id('metacpan_search-input')->type('Test::More'); $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click(); $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('page-content')->find_id('metacpan_search-input')->type('Test::More'); $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click(); $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 searches the [Password Manager](https://support.mozilla.org/en-US/kb/password-manager-remember-delete-edit-logins) 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](https://en.wikipedia.org/wiki/XPath) as the first parameter and returns the first [element](https://metacpan.org/pod/Firefox::Marionette::Element) that matches this expression. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception will be thrown. For the same functionality that returns undef if no elements are found, see the [has](#has) method. ## find\_id accepts an [id](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id) as the first parameter and returns the first [element](https://metacpan.org/pod/Firefox::Marionette::Element) with a matching 'id' property. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception will be thrown. For the same functionality that returns undef if no elements are found, see the [has\_id](#has_id) method. ## find\_name This method returns the first [element](https://metacpan.org/pod/Firefox::Marionette::Element) with a matching 'name' property. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception will be thrown. For the same functionality that returns undef if no elements are found, see the [has\_name](#has_name) method. ## find\_class accepts a [class name](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class) as the first parameter and returns the first [element](https://metacpan.org/pod/Firefox::Marionette::Element) with a matching 'class' property. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception will be thrown. For the same functionality that returns undef if no elements are found, see the [has\_class](#has_class) method. ## find\_selector accepts a [CSS Selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) as the first parameter and returns the first [element](https://metacpan.org/pod/Firefox::Marionette::Element) that matches that selector. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception will be thrown. For the same functionality that returns undef if no elements are found, see the [has\_selector](#has_selector) method. ## find\_tag accepts a [tag name](https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName) as the first parameter and returns the first [element](https://metacpan.org/pod/Firefox::Marionette::Element) with this tag name. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception will be thrown. For the same functionality that returns undef if no elements are found, see the [has\_tag](#has_tag) method. ## find\_link accepts a text string as the first parameter and returns the first link [element](https://metacpan.org/pod/Firefox::Marionette::Element) that has a matching link text. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception will be thrown. For the same functionality that returns undef if no elements are found, see the [has\_link](#has_link) method. ## find\_partial accepts a text string as the first parameter and returns the first link [element](https://metacpan.org/pod/Firefox::Marionette::Element) that has a partially matching link text. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception will be thrown. For the same functionality that returns undef if no elements are found, see the [has\_partial](#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](https://metacpan.org/pod/Firefox::Marionette::Timeouts#page_load) duration to elapse before returning, which, by default is 5 minutes. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## full\_screen full screens the firefox window. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## geo accepts an optional [geo location](https://metacpan.org/pod/Firefox::Marionette::GeoLocation) object or the parameters for a [geo location](https://metacpan.org/pod/Firefox::Marionette::GeoLocation) object, turns on the [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) and returns the current [value](https://metacpan.org/pod/Firefox::Marionette::GeoLocation) returned by calling the javascript [getCurrentPosition](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition) method. This method is further discussed in the [GEO LOCATION](#geo-location) section. If the current location cannot be determined, this method will return undef. NOTE: firefox will only allow [Geolocation](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation) calls to be made from [secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) and bizarrely, this does not include about:blank or similar. Therefore, you will need to load a page before calling the [geo](#geo) method. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new( proxy => 'https://this.is.another.location:3128', geo => 1 ); # Get geolocation for this.is.another.location (via proxy) $firefox->geo($firefox->json('https://freeipapi.com/api/json/')); # now google maps will show us in this.is.another.location $firefox->go('https://maps.google.com/'); if (my $geo = $firefox->geo()) { warn "Apparently, we're now at " . join q[, ], $geo->latitude(), $geo->longitude(); } else { warn "This computer is not allowing geolocation"; } # OR the quicker setup (run this with perl -C) warn "Apparently, we're now at " . Firefox::Marionette->new( proxy => 'https://this.is.another.location:3128', geo => 'https://freeipapi.com/api/json/' )->go('https://maps.google.com/')->geo(); NOTE: currently this call sets the location to be exactly what is specified. It will also attempt to modify the current timezone (if available in the [geo location](https://metacpan.org/pod/Firefox::Marionette::GeoLocation) parameter) to match the specified [timezone](https://metacpan.org/pod/Firefox::Marionette::GeoLocation#tz). This function should be considered experimental. Feedback welcome. If particular, the [ipgeolocation API](https://ipgeolocation.io/documentation/ip-geolocation-api.html) is the only API that currently providing geolocation data and matching timezone data in one API call. If anyone finds/develops another similar API, I would be delighted to include support for it in this module. ## go Navigates the current browsing context to the given [URI](https://metacpan.org/pod/URI) and waits for the document to load or the session's [page\_load](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](#go) method return quicker, you need to set the [page load strategy](https://metacpan.org/pod/Firefox::Marionette::Capabilities#page_load_strategy) [capability](https://metacpan.org/pod/Firefox::Marionette::Capabilities) 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](#downloading-using-go-method) for a necessary workaround and the [download](#download) method for an alternative. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## get\_pref accepts a [preference](http://kb.mozillazine.org/About:config) name. See the [set\_pref](#set_pref) and [clear\_pref](#clear_pref) methods to set a preference value and to restore it to it's original value. This method returns the current value of the preference. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(); warn "Your browser's default search engine is set to " . $firefox->get_pref('browser.search.defaultenginename'); ## har returns a hashref representing the [http archive](https://en.wikipedia.org/wiki/HAR_\(file_format\)) of the session. This function is subject to the [script](https://metacpan.org/pod/Firefox::Marionette::Timeouts#script) timeout, which, by default is 30 seconds. It is also possible for the function to hang (until the [script](https://metacpan.org/pod/Firefox::Marionette::Timeouts#script) timeout) if the original [devtools](https://developer.mozilla.org/en-US/docs/Tools) window is closed. The hashref has been designed to be accepted by the [Archive::Har](https://metacpan.org/pod/Archive::Har) module. 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->await(sub { $firefox->find_class('autocomplete-suggestion'); })->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](https://en.wikipedia.org/wiki/XPath) as the first parameter and returns the first [element](https://metacpan.org/pod/Firefox::Marionette::Element) that matches this expression. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception, see the [find](#find) method. ## has\_id accepts an [id](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id) as the first parameter and returns the first [element](https://metacpan.org/pod/Firefox::Marionette::Element) with a matching 'id' property. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception, see the [find\_id](#find_id) method. ## has\_name This method returns the first [element](https://metacpan.org/pod/Firefox::Marionette::Element) with a matching 'name' property. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception, see the [find\_name](#find_name) method. ## has\_class accepts a [class name](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class) as the first parameter and returns the first [element](https://metacpan.org/pod/Firefox::Marionette::Element) with a matching 'class' property. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception, see the [find\_class](#find_class) method. ## has\_selector accepts a [CSS Selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) as the first parameter and returns the first [element](https://metacpan.org/pod/Firefox::Marionette::Element) that matches that selector. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception, see the [find\_selector](#find_selector) method. ## has\_tag accepts a [tag name](https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName) as the first parameter and returns the first [element](https://metacpan.org/pod/Firefox::Marionette::Element) with this tag name. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception, see the [find\_tag](#find_tag) method. ## has\_link accepts a text string as the first parameter and returns the first link [element](https://metacpan.org/pod/Firefox::Marionette::Element) that has a matching link text. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception, see the [find\_link](#find_link) method. ## has\_partial accepts a text string as the first parameter and returns the first link [element](https://metacpan.org/pod/Firefox::Marionette::Element) that has a partially matching link text. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Exception::NotFound) exception, see the [find\_partial](#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](#json) method for an alternative when dealing with response content types such as application/json and [strip](#strip) for an alternative 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(); ## import\_bookmarks accepts a filesystem path to a bookmarks file and imports all the [bookmarks](https://metacpan.org/pod/Firefox::Marionette::Bookmark) in that file. It can deal with backups from [Firefox](https://support.mozilla.org/en-US/kb/export-firefox-bookmarks-to-backup-or-transfer), [Chrome](https://support.google.com/chrome/answer/96816?hl=en) or Edge. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->import_bookmarks('/path/to/bookmarks_file.html'); This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## images returns a list of all of the following elements; - [img](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) - [image inputs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/image) as [Firefox::Marionette::Image](https://metacpan.org/pod/Firefox::Marionette::Image) objects. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://developer.mozilla.org/en-US/docs/Mozilla/XPI). - path to a directory containing [firefox extension source code](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension). This directory will be packaged up as an unsigned xpi file. - path to a top level file (such as [manifest.json](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Anatomy_of_a_WebExtension#manifest.json)) in a directory containing [firefox extension source code](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension). 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](https://extensionworkshop.com/documentation/develop/temporary-installation-in-firefox/) (just for the existence of this browser instance). Unsigned xpi files [may only be loaded temporarily](https://wiki.mozilla.org/Add-ons/Extension_Signing) (except for [nightly firefox installations](https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly)). It returns the GUID for the addon which may be used as a parameter to the [uninstall](#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](#loaded) is true use Firefox::Marionette(); my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); $firefox->find_id('metacpan_search-input')->type('Type::More'); $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click(); while(!$firefox->interactive()) { # redirecting to Test::More page } ## is\_displayed accepts an [element](https://metacpan.org/pod/Firefox::Marionette::Element) as the first parameter. This method returns true or false depending on if the element [is displayed](https://firefox-source-docs.mozilla.org/testing/marionette/internals/interaction.html#interaction.isElementDisplayed). ## is\_enabled accepts an [element](https://metacpan.org/pod/Firefox::Marionette::Element) as the first parameter. This method returns true or false depending on if the element [is enabled](https://w3c.github.io/webdriver/#is-element-enabled). ## is\_selected accepts an [element](https://metacpan.org/pod/Firefox::Marionette::Element) as the first parameter. This method returns true or false depending on if the element [is selected](https://w3c.github.io/webdriver/#dfn-is-element-selected). Note that this method only makes sense for [checkbox](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox) or [radio](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio) inputs or [option](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option) elements in a [select](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select) dropdown. ## is\_trusted accepts an [certificate](https://metacpan.org/pod/Firefox::Marionette::Certificate) as the first parameter. This method returns true or false depending on if the certificate is a trusted CA certificate in the current profile. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new( profile_name => 'default' ); foreach my $certificate ($firefox->certificates()) { if (($certificate->is_ca_cert()) && ($firefox->is_trusted($certificate))) { say $certificate->display_name() . " is a trusted CA cert in the current profile"; } } ## json returns a [JSON](https://metacpan.org/pod/JSON) object that has been parsed from the page source of the content document. This is a convenience method that wraps the [strip](#strip) method. use Firefox::Marionette(); use v5.10; say Firefox::Marionette->new()->go('https://fastapi.metacpan.org/v1/download_url/Firefox::Marionette")->json()->{version}; In addition, this method can accept a [URI](https://metacpan.org/pod/URI) as a parameter and retrieve that URI via the firefox [fetch call](https://developer.mozilla.org/en-US/docs/Web/API/fetch) and transforming the body to [JSON via firefox](https://developer.mozilla.org/en-US/docs/Web/API/Response/json_static) use Firefox::Marionette(); use v5.10; say Firefox::Marionette->new()->json('https://freeipapi.com/api/json/')->{ipAddress}; ## key\_down accepts a parameter describing a key and returns an action for use in the [perform](#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](#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(); ## languages accepts an optional list of values for the [Accept-Language](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) header and sets this using the profile preferences. It returns the current values as a list, such as ('en-US', 'en'). ## loaded returns true if `document.readyState === "complete"` use Firefox::Marionette(); my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); $firefox->find_id('metacpan_search-input')->type('Type::More'); $firefox->await(sub { $firefox->find_class('autocomplete-suggestion'); })->click(); while(!$firefox->loaded()) { # redirecting to Test::More page } ## logins returns a list of all [Firefox::Marionette::Login](https://metacpan.org/pod/Firefox::Marionette::Login) objects available. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new(); foreach my $login ($firefox->logins()) { say "Found login for " . $login->host() . " and user " . $login->user(); } ## 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](https://bitwarden.com/help/article/condition-bitwarden-import/) - [LastPass CSV](https://support.logmeininc.com/lastpass/help/how-do-i-nbsp-export-stored-data-from-lastpass-using-a-generic-csv-file) - [KeePass CSV](https://keepass.info/help/base/importexport.html#csv) returns a list of [Firefox::Marionette::Login](https://metacpan.org/pod/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\_xml accepts a filehandle as a parameter and then reads the filehandle for exported logins as XML. This is known to work with the following formats; - [KeePass 1.x XML](https://keepass.info/help/base/importexport.html#xml) returns a list of [Firefox::Marionette::Login](https://metacpan.org/pod/Firefox::Marionette::Login) objects. use Firefox::Marionette(); use FileHandle(); my $handle = FileHandle->new('/path/to/keepass1.xml'); 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](https://support.1password.com/1pux-format/) returns a list of [Firefox::Marionette::Login](https://metacpan.org/pod/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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) - [area](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area) - [frame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/frame) - [iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) - [meta](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) as [Firefox::Marionette::Link](https://metacpan.org/pod/Firefox::Marionette::Link) objects. This method is subject to the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://en.wikipedia.org/wiki/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](https://metacpan.org/pod/Firefox::Marionette) 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](#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](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## mouse\_down accepts a parameter describing which mouse button the method should apply to ([left](https://metacpan.org/pod/Firefox::Marionette::Buttons#LEFT), [middle](https://metacpan.org/pod/Firefox::Marionette::Buttons#MIDDLE) or [right](https://metacpan.org/pod/Firefox::Marionette::Buttons#RIGHT)) and returns an action for use in the [perform](#perform) method that corresponding with a mouse button being depressed. ## mouse\_move accepts a [element](https://metacpan.org/pod/Firefox::Marionette::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](#perform) method that corresponding with such a mouse movement, either to the specified co-ordinates or to the middle of the supplied [element](https://metacpan.org/pod/Firefox::Marionette::Element) parameter. Other parameters that may be passed are listed below; - origin - the origin of the C(<x => 0, y => 0)> co-ordinates. Should be either `viewport`, `pointer` or an [element](https://metacpan.org/pod/Firefox::Marionette::Element). - duration - Number of milliseconds over which to distribute the move. If not defined, the duration defaults to 0. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## mouse\_up accepts a parameter describing which mouse button the method should apply to ([left](https://metacpan.org/pod/Firefox::Marionette::Buttons#LEFT), [middle](https://metacpan.org/pod/Firefox::Marionette::Buttons#MIDDLE) or [right](https://metacpan.org/pod/Firefox::Marionette::Buttons#RIGHT)) and returns an action for use in the [perform](#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](https://firefox.org/) binary, rather than the default path. - capabilities - use the supplied [capabilities](https://metacpan.org/pod/Firefox::Marionette::Capabilities) object, for example to set whether the browser should [accept insecure certs](https://metacpan.org/pod/Firefox::Marionette::Capabilities#accept_insecure_certs) or whether the browser should use a [proxy](https://metacpan.org/pod/Firefox::Marionette::Proxy). - chatty - Firefox is extremely chatty on the network, including checking for the latest 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](https://bugzilla.mozilla.org/show_bug.cgi?id=1598562#c13). The only way to prevent this seems to be to set firefox.settings.services.mozilla.com to 127.0.0.1 via [/etc/hosts](https://en.wikipedia.org/wiki//etc/hosts). NOTE: that this option only works when profile\_name/profile is not specified. - console - show the [browser console](https://developer.mozilla.org/en-US/docs/Tools/Browser_Console/) when the browser is launched. This defaults to "0" (off). See [CONSOLE LOGGING](#console-logging) for a discussion of how to send log messages to the console. - 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](#debug) method. If this option is not a boolean (0|1), the value will be passed to the [MOZ\_LOG](https://firefox-source-docs.mozilla.org/networking/http/logging.html) option on the command line of the firefox binary to allow extra levels of debug. - developer - only allow a [developer edition](https://www.mozilla.org/en-US/firefox/developer/) to be launched. This defaults to "0" (off). - devtools - begin the session with the [devtools](https://developer.mozilla.org/en-US/docs/Tools) window opened in a separate window. - geo - setup the browser [preferences](http://kb.mozillazine.org/About:config) to allow the [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) to work. If the value for this key is a [URI](https://metacpan.org/pod/URI) object or a string beginning with '^(?:data|http)', this object will be retrieved using the [json](#json) method and the response will used to build a [GeoLocation](https://metacpan.org/pod/Firefox::Mozilla::GeoLocation) object, which will be sent to the [geo](#geo) method. If the value for this key is a hash, the hash will be used to build a [GeoLocation](https://metacpan.org/pod/Firefox::Mozilla::GeoLocation) object, which will be sent to the [geo](#geo) method. - height - set the [height](http://kb.mozillazine.org/Command_line_arguments#List_of_command_line_arguments_.28incomplete.29) of the initial firefox window - har - begin the session with the [devtools](https://developer.mozilla.org/en-US/docs/Tools) window opened in a separate window. The [HAR Export Trigger](https://addons.mozilla.org/en-US/firefox/addon/har-export-trigger/) addon will be loaded into the new session automatically, which means that [-safe-mode](http://kb.mozillazine.org/Command_line_arguments#List_of_command_line_arguments_.28incomplete.29) will not be activated for this session AND this functionality will only be available for Firefox 61+. - host - use [ssh](https://man.openbsd.org/ssh.1) to create and automate firefox on the specified host. See [REMOTE AUTOMATION OF FIREFOX VIA SSH](#remote-automation-of-firefox-via-ssh) and [NETWORK ARCHITECTURE](#network-architecture). The user will default to the current user name (see the user parameter to change this). Authentication should be via public keys loaded into the local [ssh-agent](https://man.openbsd.org/ssh-agent). - implicit - a shortcut to allow directly providing the [implicit](https://metacpan.org/pod/Firefox::Marionette::Timeout#implicit) timeout, instead of needing to use timeouts from the capabilities parameter. Overrides all longer ways. - index - a parameter to allow the user to specify a specific firefox instance to survive and reconnect to. It does not do anything else at the moment. See the survive parameter. - kiosk - start the browser in [kiosk](https://support.mozilla.org/en-US/kb/firefox-enterprise-kiosk-mode) 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](https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly) to be launched. This defaults to "0" (off). - port - if the "host" parameter is also set, use [ssh](https://man.openbsd.org/ssh.1) to create and automate firefox via the specified port. See [REMOTE AUTOMATION OF FIREFOX VIA SSH](#remote-automation-of-firefox-via-ssh) and [NETWORK ARCHITECTURE](#network-architecture). - page\_load - a shortcut to allow directly providing the [page\_load](https://metacpan.org/pod/Firefox::Marionette::Timeouts#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](https://metacpan.org/pod/Firefox::Marionette::Profile). NOTE: firefox ignores any changes made to the profile on the disk while it is running, instead, use the [set\_pref](#set_pref) and [clear\_pref](#clear_pref) methods to make changes while firefox is running. - profile\_name - pick a specific existing profile to automate, rather than creating a new profile. [Firefox](https://firefox.com) 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()](https://metacpan.org/pod/Firefox::Marionette::Profile#names) method. The following conditions are required to use existing profiles; - the preference `security.webauth.webauthn_enable_softtoken` must be set to `true` in the profile OR - the `webauth` parameter to this method must be set to `0` NOTE: firefox ignores any changes made to the profile on the disk while it is running, instead, use the [set\_pref](#set_pref) and [clear\_pref](#clear_pref) methods to make changes while firefox is running. - proxy - this is a shortcut method for setting a [proxy](https://metacpan.org/pod/Firefox::Marionette::Proxy) using the [capabilities](https://metacpan.org/pod/Firefox::Marionette::Capabilities) parameter above. It accepts a proxy URL, with the following allowable schemes, 'http' and 'https'. It also allows a reference to a list of proxy URLs which will function as list of proxies that Firefox will try in [left to right order](https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file#description) until a working proxy is found. See [REMOTE AUTOMATION OF FIREFOX VIA SSH](#remote-automation-of-firefox-via-ssh), [NETWORK ARCHITECTURE](#network-architecture) and [SETTING UP SOCKS SERVERS USING SSH](https://metacpan.org/pod/Firefox::Marionette::Proxy#SETTING-UP-SOCKS-SERVERS-USING-SSH). - reconnect - an experimental parameter to allow a reconnection to firefox that a connection has been discontinued. See the survive parameter. - scp - force the scp protocol when transferring files to remote hosts via ssh. See [REMOTE AUTOMATION OF FIREFOX VIA SSH](#remote-automation-of-firefox-via-ssh) and the --scp-only option in the [ssh-auth-cmd-marionette](https://metacpan.org/pod/ssh-auth-cmd-marionette) script in this distribution. - script - a shortcut to allow directly providing the [script](https://metacpan.org/pod/Firefox::Marionette::Timeout#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](#await) or [bye](#bye) methods. This defaults to "1" millisecond. - stealth - stops [navigator.webdriver](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/webdriver) from being accessible by the current web page. This is achieved by loading an [extension](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions), which will automatically switch on the `addons` parameter for the [new](#new) method. This is extremely experimental. See [IMITATING OTHER BROWSERS](#imitating-other-browsers) for a discussion. - 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](https://en.wikipedia.org/wiki/Root_certificate) encoded as a [PEM encoded X.509 certificate](https://datatracker.ietf.org/doc/html/rfc7468#section-5) that will be trusted for this session. - timeouts - a shortcut to allow directly providing a [timeout](https://metacpan.org/pod/Firefox::Marionette::Timeout) object, instead of needing to use timeouts from the capabilities parameter. Overrides the timeouts provided (if any) in the capabilities parameter. - trackable - if this is set, profile preferences will be [set](#set_pref) to make it harder to be tracked by the [browsers fingerprint](https://en.wikipedia.org/wiki/Device_fingerprint#Browser_fingerprint) across browser restarts. This is on by default, but may be switched off by setting it to 0; - user - if the "host" parameter is also set, use [ssh](https://man.openbsd.org/ssh.1) to create and automate firefox with the specified user. See [REMOTE AUTOMATION OF FIREFOX VIA SSH](#remote-automation-of-firefox-via-ssh) and [NETWORK ARCHITECTURE](#network-architecture). The user will default to the current user name. Authentication should be via public keys loaded into the local [ssh-agent](https://man.openbsd.org/ssh-agent). - via - specifies a [proxy jump box](https://man.openbsd.org/ssh_config#ProxyJump) to be used to connect to a remote host. See the host parameter. - visible - should firefox be visible on the desktop. This defaults to "0". When moving from a X11 platform to another X11 platform, you can set visible to 'local' to enable [X11 forwarding](https://man.openbsd.org/ssh#X). See [X11 FORWARDING WITH FIREFOX](#x11-forwarding-with-firefox). - waterfox - only allow a binary that looks like a [waterfox version](https://www.waterfox.net/) to be launched. - webauthn - a boolean parameter to determine whether or not to [add a webauthn authenticator](#add_webauthn_authenticator) after the connection is established. The default is to add a webauthn authenticator for Firefox after version 118. - width - set the [width](http://kb.mozillazine.org/Command_line_arguments#List_of_command_line_arguments_.28incomplete.29) of the initial firefox window This method returns a new `Firefox::Marionette` object, connected to an instance of [firefox](https://firefox.com). 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](https://en.wikipedia.org/wiki/Xvfb) instance. use Firefox::Marionette(); my $remote_darwin_firefox = Firefox::Marionette->new( debug => 'timestamp,nsHttp: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](https://metacpan.org/pod/Firefox::Marionette::WebWindow) 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](#browser_version) of firefox is a [nightly release](https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly) (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](#perform) method that will cause a pause in the chain of actions given to the [perform](#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](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/96#webdriver_conformance_marionette) 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. `shrink_to_fit` should be disabled to make `scale` work. - size - The desired size (width and height) of the pdf, specified by name. See the page key for an alternative and the [paper\_sizes](#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](https://metacpan.org/pod/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); ... } ## percentage\_visible accepts an [element](https://metacpan.org/pod/Firefox::Marionette::Element) as the first parameter and returns the percentage of that [element](https://metacpan.org/pod/Firefox::Marionette::Element) that is currently visible in the [viewport](https://developer.mozilla.org/en-US/docs/Glossary/Viewport). It achieves this by determining the co-ordinates of the [DOMRect](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect) with a [getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) call and then using [elementsFromPoint](https://developer.mozilla.org/en-US/docs/Web/API/Document/elementsFromPoint) and [getComputedStyle](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle) calls to determine how the percentage of the [DOMRect](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect) that is visible to the user. The [getComputedStyle](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle) call is used to determine the state of the [visibility](https://developer.mozilla.org/en-US/docs/Web/CSS/visibility) and [display](https://developer.mozilla.org/en-US/docs/Web/CSS/display) attributes. use Firefox::Marionette(); use Encode(); use v5.10; my $firefox = Firefox::Marionette->new( visible => 1, kiosk => 1 )->go('http://metacpan.org');; my $element = $firefox->find_id('metacpan_search-input'); my $totally_viewable_percentage = $firefox->percentage_visible($element); # search box is slightly hidden by different effects foreach my $display ($firefox->displays()) { if ($firefox->resize($display->width(), $display->height())) { if ($firefox->percentage_visible($element) < $totally_viewable_percentage) { say 'Search box stops being fully viewable with ' . Encode::encode('UTF-8', $display->usage()); last; } } } ## perform accepts a list of actions (see [mouse\_up](#mouse_up), [mouse\_down](#mouse_down), [mouse\_move](#mouse_move), [pause](#pause), [key\_down](#key_down) and [key\_up](#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](#release) method will complete outstanding actions (such as [mouse\_up](#mouse_up) or [key\_up](#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](#release) method for an alternative for manually specifying all the [mouse\_up](#mouse_up) and [key\_up](#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](https://metacpan.org/pod/Firefox::Marionette::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](#attribute) method will return the initial content from the HTML source code. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); my $element = $firefox->find_id('metacpan_search-input'); $element->property('value') eq '' or die "Initial property should be 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](https://support.mozilla.org/en-US/kb/use-primary-password-protect-stored-logins) and locks the [Password Manager](https://support.mozilla.org/en-US/kb/password-manager-remember-delete-edit-logins) 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](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## pwd\_mgr\_login Accepts the [primary password](https://support.mozilla.org/en-US/kb/use-primary-password-protect-stored-logins) and allows the user to access the [Password Manager](https://support.mozilla.org/en-US/kb/password-manager-remember-delete-edit-logins). 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](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## pwd\_mgr\_logout Logs the user out of being able to access the [Password Manager](https://support.mozilla.org/en-US/kb/password-manager-remember-delete-edit-logins). 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](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## pwd\_mgr\_needs\_login returns true or false if the [Password Manager](https://support.mozilla.org/en-US/kb/password-manager-remember-delete-edit-logins) has been locked and needs a [primary password](https://support.mozilla.org/en-US/kb/use-primary-password-protect-stored-logins) 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](https://metacpan.org/pod/Firefox::Marionette::Element) as the first parameter and returns the current [position and size](https://metacpan.org/pod/Firefox::Marionette::Element::Rect) of the [element](https://metacpan.org/pod/Firefox::Marionette::Element) ## refresh refreshes the current page. The browser will wait for the page to completely refresh or the session's [page\_load](https://metacpan.org/pod/Firefox::Marionette::Timeouts#page_load) duration to elapse before returning, which, by default is 5 minutes. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## release completes any outstanding actions issued by the [perform](#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(); ## resize accepts width and height parameters in a list and then attempts to resize the entire browser to match those parameters. Due to the oddities of various window managers, this function needs to manually calculate what the maximum and minimum sizes of the display is. It does this by; - 1 performing a [maximise](https://metacpan.org/pod/Firefox::Marionette::maximise), then - 2 caching the browser's current width and height as the maximum width and height. It - 3 then calls [resizeTo](https://developer.mozilla.org/en-US/docs/Web/API/Window/resizeTo) to resize the window to 0,0 - 4 wait for the browser to send a [resize](https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event) event. - 5 cache the browser's current width and height as the minimum width and height - 6 if the requested width and height are outside of the maximum and minimum widths and heights return false - 7 if the requested width and height matches the current width and height return [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. Otherwise, - 8 call [resizeTo](https://developer.mozilla.org/en-US/docs/Web/API/Window/resizeTo) for the requested width and height - 9 wait for the [resize](https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event) event This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods if the method succeeds, otherwise it returns false. use Firefox::Marionette(); use Encode(); use v5.10; my $firefox = Firefox::Marionette->new( visible => 1, kiosk => 1 )->go('http://metacpan.org');; if ($firefox->resize(1024, 768)) { say 'We are showing an XGA display'; } else { say 'Resize failed to work'; } ## resolve accepts a hostname as an argument and resolves it to a list of matching IP addresses. It can also accept an optional hash containing additional keys, described in [Firefox::Marionette::DNS](https://metacpan.org/pod/Firefox::Marionette::DNS). use Firefox::Marionette(); use v5.10; my $ssh_server = 'remote.example.org'; my $firefox = Firefox::Marionette->new( host => $ssh_server ); my $hostname = 'metacpan.org'; foreach my $ip_address ($firefox->resolve($hostname)) { say "$hostname resolves to $ip_address at $ssh_server"; } $firefox = Firefox::Marionette->new(); foreach my $ip_address ($firefox->resolve($hostname, flags => Firefox::Marionette::DNS::RESOLVE_REFRESH_CACHE() | Firefox::Marionette::DNS::RESOLVE_BYPASS_CACHE(), type => Firefox::Marionette::DNS::RESOLVE_TYPE_DEFAULT())) { say "$hostname resolves to $ip_address; } ## resolve\_override accepts a hostname and an IP address as parameters. This method then forces the browser to override any future DNS requests for the supplied hostname. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new(); my $hostname = 'metacpan.org'; my $ip_address = '127.0.0.1'; foreach my $result ($firefox->resolve_override($hostname, $ip_address)->resolve($hostname)) { if ($result eq $ip_address) { warn "local metacpan time?"; } else { die "This should not happen"; } } $firefox->go('https://metacpan.org'); # this tries to contact a webserver on 127.0.0.1 This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## restart restarts the browser. After the restart, [capabilities](https://metacpan.org/pod/Firefox::Marionette::Capabilities) should be restored. The same profile settings should be applied, but the current state of the browser (such as the [uri](#uri) will be reset (like after a normal browser restart). This method is primarily intended for use by the [update](#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](https://metacpan.org/pod/Firefox::Marionette) 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 evaluated 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](https://developer.mozilla.org/en-US/docs/Web/API/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 evaluated in a sandbox with elevated system privileges, equivalent to chrome space. - timeout - A timeout to override the default [script](https://metacpan.org/pod/Firefox::Marionette::Timeouts#script) timeout, which, by default is 30 seconds. Returns the result of the javascript function. When a parameter is an [element](https://metacpan.org/pod/Firefox::Marionette::Element) (such as being returned from a [find](#find) type operation), the [script](#script) method will automatically translate that into a javascript object. Likewise, when the result being returned in a [script](#script) method is an [element](https://dom.spec.whatwg.org/#concept-element) it will be automatically translated into a [perl object](https://metacpan.org/pod/Firefox::Marionette::Element). use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new()->go('https://metacpan.org/'); if (my $element = $firefox->script('return document.getElementsByName("metacpan_search-input")[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](https://metacpan.org/pod/Firefox::Marionette::Timeouts#script) timeout, which, by default is 30 seconds. ## selfie returns a [File::Temp](https://metacpan.org/pod/File::Temp) object containing a lossless PNG image screenshot. If an [element](https://metacpan.org/pod/Firefox::Marionette::Element) is passed as a parameter, the screenshot will be restricted to the element. If an [element](https://metacpan.org/pod/Firefox::Marionette::Element) is not passed as a parameter and the current [context](#context) is 'chrome', a screenshot of the current viewport will be returned. If an [element](https://metacpan.org/pod/Firefox::Marionette::Element) is not passed as a parameter and the current [context](#context) is 'content', a screenshot of the current frame will be returned. The parameters after the [element](https://metacpan.org/pod/Firefox::Marionette::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](https://metacpan.org/pod/Firefox::Marionette::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](https://metacpan.org/pod/Firefox::Marionette::Element) supplied - highlights - a reference to a list containing [elements](https://metacpan.org/pod/Firefox::Marionette::Element) to draw a highlight around. Not available in [Firefox 70](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/70#WebDriver_conformance_Marionette) onwards. ## scroll accepts a [element](https://metacpan.org/pod/Firefox::Marionette::Element) as the first parameter and [scrolls](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) to it. The optional second parameter is the same as for the [scrollInfoView](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) method. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(visible => 1)->go('https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView'); my $link = $firefox->find_id('content')->find_link('Examples'); $firefox->scroll($link); $firefox->scroll($link, 1); $firefox->scroll($link, { behavior => 'smooth', block => 'center' }); $firefox->scroll($link, { block => 'end', inline => 'nearest' }); ## send\_alert\_text sends keys to the input field of a currently displayed modal message box ## set\_javascript accepts a parameter for the the profile preference value of [javascript.enabled](https://support.mozilla.org/en-US/kb/javascript-settings-for-interactive-web-pages#w_for-advanced-users). This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## set\_pref accepts a [preference](http://kb.mozillazine.org/About:config) name and the new value to set it to. See the [get\_pref](#get_pref) and [clear\_pref](#clear_pref) methods to get a preference value and to restore it to it's original value. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(); ... $firefox->set_pref('browser.search.defaultenginename', 'DuckDuckGo'); ## shadow\_root accepts an [element](https://metacpan.org/pod/Firefox::Marionette::Element) as a parameter and returns it's [ShadowRoot](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) as a [shadow root](https://metacpan.org/pod/Firefox::Marionette::ShadowRoot) 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(); } See the [FINDING ELEMENTS IN A SHADOW DOM](#finding-elements-in-a-shadow-dom) section for how to delve into a [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM). ## shadowy accepts an [element](https://metacpan.org/pod/Firefox::Marionette::Element) as a parameter and returns true if the element has a [ShadowRoot](https://developer.mozilla.org/en-US/docs/Web/API/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](https://metacpan.org/pod/Firefox::Marionette::Element#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](#await) or [bye](#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](#json) method for an alternative when dealing with response content types such as application/json and [html](#html) for an alternative when dealing with html content types. This is a convenience method that wraps the [html](#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](#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](https://metacpan.org/pod/Firefox::Marionette::Element) 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](https://metacpan.org/pod/Firefox::Marionette::WebWindow) (either the result of [window\_handles](#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 = $firefox->window_handle(); $firefox->new_window( type => 'tab' ); $firefox->new_window( type => 'window' ); $firefox->switch_to_window($original_window); $firefox->go('https://metacpan.org'); ## tag\_name accepts a [Firefox::Marionette::Element](https://metacpan.org/pod/Firefox::Marionette::Element) object as the first parameter and returns the relevant tag name. For example '[a](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)' or '[input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)'. ## text accepts a [element](https://metacpan.org/pod/Firefox::Marionette::Element) as the first parameter and returns the text that is contained by that element (if any) ## timeouts returns the current [timeouts](https://metacpan.org/pod/Firefox::Marionette::Timeouts) for page loading, searching, and scripts. ## tz accepts a [Olson TZ identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) as the first parameter. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## title returns the current [title](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title) of the window. ## type accepts an [element](https://metacpan.org/pod/Firefox::Marionette::Element) as the first parameter and a string as the second parameter. It sends the string to the specified [element](https://metacpan.org/pod/Firefox::Marionette::Element) in the current page, such as filling out a text box. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. ## uname returns the $^O ($OSNAME) compatible string to describe the platform where firefox is running. ## update queries the Update Services and applies any available updates. [Restarts](#restart) 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](https://metacpan.org/pod/Firefox::Marionette::UpdateStatus) 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](#install) method. This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) 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](https://metacpan.org/pod/URI) of current top level browsing context for Desktop. It is equivalent to the javascript `document.location.href` ## webauthn\_authenticator returns the default [WebAuthn authenticator](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator) created when the [new](#new) method was called. ## webauthn\_credentials This method accepts an optional [authenticator](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator), in which case it will return all the [credentials](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Credential) attached to this authenticator. If no parameter is supplied, [credentials](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Credential) from the default authenticator will be returned. use Firefox::Marionette(); use v5.10; my $firefox = Firefox::Marionette->new(); foreach my $credential ($firefox->webauthn_credentials()) { say "Credential host is " . $credential->host(); } # OR my $authenticator = $firefox->add_webauthn_authenticator( transport => Firefox::Marionette::WebAuthn::Authenticator::INTERNAL(), protocol => Firefox::Marionette::WebAuthn::Authenticator::CTAP2() ); foreach my $credential ($firefox->webauthn_credentials($authenticator)) { say "Credential host is " . $credential->host(); } ## webauthn\_set\_user\_verified This method accepts a boolean for the [is\_user\_verified](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#is_user_verified) field and an optional [authenticator](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator) (the default authenticator will be used otherwise). It sets the [is\_user\_verified](https://metacpan.org/pod/Firefox::Marionette::WebAuthn::Authenticator#is_user_verified) field to the supplied boolean value. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(); $firefox->webauthn_set_user_verified(1); ## wheel accepts a [element](https://metacpan.org/pod/Firefox::Marionette::Element) parameter, or a `( x => 0, y => 0 )` type hash manually describing exactly where to move the mouse from and returns an action for use in the [perform](#perform) method that corresponding with such a wheel action, either to the specified co-ordinates or to the middle of the supplied [element](https://metacpan.org/pod/Firefox::Marionette::Element) parameter. Other parameters that may be passed are listed below; - origin - the origin of the C(<x => 0, y => 0)> co-ordinates. Should be either `viewport`, `pointer` or an [element](https://metacpan.org/pod/Firefox::Marionette::Element). - duration - Number of milliseconds over which to distribute the move. If not defined, the duration defaults to 0. - deltaX - the change in X co-ordinates during the wheel. If not defined, deltaX defaults to 0. - deltaY - the change in Y co-ordinates during the wheel. If not defined, deltaY defaults to 0. ## 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 existence of this product. Only of interest when sub-classing. ## window\_handle returns the [current window's handle](https://metacpan.org/pod/Firefox::Marionette::WebWindow). 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. This is the same as the [window](https://developer.mozilla.org/en-US/docs/Web/API/Window) object in Javascript. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new(); my $original_window = $firefox->window_handle(); my $javascript_window = $firefox->script('return window'); # only works for Firefox 121 and later if ($javascript_window ne $original_window) { die "That was unexpected!!! What happened?"; } ## window\_handles returns a list of top-level [browsing contexts](https://metacpan.org/pod/Firefox::Marionette::WebWindow). 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 = $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](https://metacpan.org/pod/Firefox::Marionette::Window::Rect) as a parameter, sets the current browser window to that position and size and returns the previous [position, size and state](https://metacpan.org/pod/Firefox::Marionette::Window::Rect) of the browser window. If no parameter is supplied, it returns the current [position, size and state](https://metacpan.org/pod/Firefox::Marionette::Window::Rect) 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 # NETWORK ARCHITECTURE This module allows for a complicated network architecture, including SSH and HTTP proxies. my $firefox = Firefox::Marionette->new( host => 'Firefox.runs.here' via => 'SSH.Jump.Box', trust => '/path/to/ca-for-squid-proxy-server.crt', proxy => 'https://Squid.Proxy.Server:3128' )->go('https://Target.Web.Site'); produces the following effect, with an ascii box representing a separate network node. --------- ---------- ----------- | Perl | SSH | SSH | SSH | Firefox | | runs |--------->| Jump |-------->| runs | | here | | Box | | here | --------- ---------- ----------- | ---------- ---------- | | Target | HTTPS | Squid | TLS | | Web |<---------| Proxy |<------------- | Site | | Server | ---------- ---------- In addition, the proxy parameter can be used to specify multiple proxies using a reference to a list. my $firefox = Firefox::Marionette->new( host => 'Firefox.runs.here' trust => '/path/to/ca-for-squid-proxy-server.crt', proxy => [ 'https://Squid1.Proxy.Server:3128', 'https://Squid2.Proxy.Server:3128' ] )->go('https://Target.Web.Site'); When firefox gets a list of proxies, it will use the first one that works. In addition, it will perform a basic form of proxy failover, which may involve a failed network request before it fails over to the next proxy. In the diagram below, Squid1.Proxy.Server is the first proxy in the list and will be used exclusively, unless it is unavailable, in which case Squid2.Proxy.Server will be used. ---------- TLS | Squid1 | ------>| Proxy |----- | | Server | | --------- ----------- | ---------- | ----------- | Perl | SSH | Firefox | | | HTTPS | Target | | runs |----->| runs |----| ------->| Web | | here | | here | | | | Site | --------- ----------- | ---------- | ----------- | TLS | Squid2 | | ------>| Proxy |----- | Server | ---------- See the [REMOTE AUTOMATION OF FIREFOX VIA SSH](#remote-automation-of-firefox-via-ssh) section for more options. See [SETTING UP SOCKS SERVERS USING SSH](https://metacpan.org/pod/Firefox::Marionette::Proxy#SETTING-UP-SOCKS-SERVERS-USING-SSH) for easy proxying via [ssh](https://man.openbsd.org/ssh) See [GEO LOCATION](#geo-location) section for how to combine this with providing appropriate browser settings for the end point. # 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 # GEO LOCATION The firefox [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) can be used by supplying the `geo` parameter to the [new](#new) method and then calling the [geo](#geo) method (from a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts)). The [geo](#geo) method can accept various specific latitude and longitude parameters as a list, such as; $firefox->geo(latitude => -37.82896, longitude => 144.9811); OR $firefox->geo(lat => -37.82896, long => 144.9811); OR $firefox->geo(lat => -37.82896, lng => 144.9811); OR $firefox->geo(lat => -37.82896, lon => 144.9811); or it can be passed in as a reference, such as; $firefox->geo({ latitude => -37.82896, longitude => 144.9811 }); the combination of a variety of parameter names and the ability to pass parameters in as a reference means it can be deal with various geo location websites, such as; $firefox->geo($firefox->json('https://freeipapi.com/api/json/')); # get geo location from current IP address $firefox->geo($firefox->json('https://geocode.maps.co/search?street=101+Collins+St&city=Melbourne&state=VIC&postalcode=3000&country=AU&format=json')->[0]); # get geo location of street address $firefox->geo($firefox->json('http://api.positionstack.com/v1/forward?access_key=' . $access_key . '&query=101+Collins+St,Melbourne,VIC+3000')->{data}->[0]); # get geo location of street address using api key $firefox->geo($firefox->json('https://api.ipgeolocation.io/ipgeo?apiKey=' . $api_key)); # get geo location from current IP address $firefox->geo($firefox->json('http://api.ipstack.com/142.250.70.206?access_key=' . $api_key)); # get geo location from specific IP address (http access only for free) These sites were active at the time this documentation was written, but mainly function as an illustration of the flexibility of [geo](#geo) and [json](#json) methods in providing the desired location to the [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API). As mentioned in the [geo](#geo) method documentation, the [ipgeolocation API](https://ipgeolocation.io/documentation/ip-geolocation-api.html) is the only API that currently providing geolocation data and matching timezone data in one API call. If this url is used, the [tz](#tz) method will be automatically called to set the timezone to the matching timezone for the geographic location. # CONSOLE LOGGING Sending debug to the console can be quite confusing in firefox, as some techniques won't work in [chrome](#chrome) context. The following example can be quite useful. use Firefox::Marionette(); my $firefox = Firefox::Marionette->new( visible => 1, devtools => 1, console => 1, devtools => 1 ); $firefox->script( q[console.log("This goes to devtools b/c it's being generated in content mode")]); $firefox->chrome()->script( q[console.log("Sent out on standard error for Firefox 136 and later")]); # 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/'); # OR use a proxy host to jump via to the final host my $firefox = Firefox::Marionette->new( host => 'remote.example.org', port => 2222, via => 'user@secure-jump-box.example.org:42222', 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](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse), 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](https://man.openbsd.org/sshd#command=_command_) option in the [OpenSSH](https://www.openssh.com/) [authorized\_keys](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT) 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](https://metacpan.org/pod/ssh-auth-cmd-marionette) command is provided as part of this distribution. The module will expect to access private keys via the local [ssh-agent](https://man.openbsd.org/ssh-agent) when authenticating. When using ssh, Firefox::Marionette will attempt to pass the [TMPDIR](https://en.wikipedia.org/wiki/TMPDIR) environment variable across the ssh connection to make cleanups easier. In order to allow this, the [AcceptEnv](https://man.openbsd.org/sshd_config#AcceptEnv) setting in the remote [sshd configuration](https://man.openbsd.org/sshd_config) should be set to allow TMPDIR, which will look like; AcceptEnv TMPDIR This module uses [ControlMaster](https://man.openbsd.org/ssh_config#ControlMaster) functionality when using [ssh](https://man.openbsd.org/ssh), for a useful speedup of executing remote commands. Unfortunately, when using ssh to move from a [cygwin](https://gcc.gnu.org/wiki/SSH_connection_caching), [Windows 10 or Windows Server 2019](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse) node to a remote environment, we cannot use [ControlMaster](https://man.openbsd.org/ssh_config#ControlMaster), because at this time, Windows [does not support ControlMaster](https://github.com/Microsoft/vscode-remote-release/issues/96) and therefore this type of automation is still possible, but slower than other client platforms. The [NETWORK ARCHITECTURE](#network-architecture) section has an example of a more complicated network design. # WEBGL There are a number of steps to getting [WebGL](https://en.wikipedia.org/wiki/WebGL) to work correctly; - 1. The `addons` parameter to the [new](#new) method must be set. This will disable [-safe-mode](http://kb.mozillazine.org/Command_line_arguments#List_of_command_line_arguments_.28incomplete.29) - 2. The visible parameter to the [new](#new) method must be set. This is due to [an existing bug in Firefox](https://bugzilla.mozilla.org/show_bug.cgi?id=1375585). - 3. It can be tricky getting [WebGL](https://en.wikipedia.org/wiki/WebGL) to work with a [Xvfb](https://en.wikipedia.org/wiki/Xvfb) instance. [glxinfo](https://dri.freedesktop.org/wiki/glxinfo/) can be useful to help debug issues in this case. The mesa-dri-drivers rpm is also required for Redhat systems. With all those conditions being met, [WebGL](https://en.wikipedia.org/wiki/WebGL) can be enabled like so; use Firefox::Marionette(); my $firefox = Firefox::Marionette->new( addons => 1, visible => 1 ); if ($firefox->script(q[let c = document.createElement('canvas'); return c.getContext('webgl2') ? true : c.getContext('experimental-webgl') ? true : false;])) { $firefox->go("https://get.webgl.org/"); } else { die "WebGL is not supported"; } # FINDING ELEMENTS IN A SHADOW DOM One aspect of [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) is the [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM). When you need to explore the structure of a [custom element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements), you need to access it via the shadow DOM. The following is an example of navigating the shadow DOM via a html file included in the test suite of this package. use Firefox::Marionette(); use Cwd(); my $firefox = Firefox::Marionette->new(); my $firefox_marionette_directory = Cwd::cwd(); $firefox->go("file://$firefox_marionette_directory/t/data/elements.html"); my $shadow_root = $firefox->find_tag('custom-square')->shadow_root(); my $outer_div = $firefox->find_id('outer-div', $shadow_root); So, this module is designed to allow you to navigate the shadow DOM using normal find methods, but you must get the shadow element's shadow root and use that as the root for the search into the shadow DOM. An important caveat is that [xpath](https://bugzilla.mozilla.org/show_bug.cgi?id=1822311) and [tag name](https://bugzilla.mozilla.org/show_bug.cgi?id=1822321) strategies do not officially work yet (and also the class name and name strategies). This module works around the tag name, class name and name deficiencies by using the matching [css selector](#find_selector) search if the original search throws a recognisable exception. Therefore these cases may be considered to be extremely experimental and subject to change when Firefox gets the "correct" functionality. # IMITATING OTHER BROWSERS There are a collection of methods and techniques that may be useful if you would like to change your geographic location or how the browser appears to your web site. - the `stealth` parameter of the [new](#new) method. This method will stop the browser reporting itself as a robot and will also (when combined with the [agent](#agent) method, change other javascript characteristics to match the [User Agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) string. - the [agent](#agent) method, which if supplied a recognisable [User Agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent), will attempt to change other attributes to match the desired browser. This is extremely experimental and feedback is welcome. - the [geo](#geo) method, which allows the modification of the [Geolocation](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation) reported by the browser, but not the location produced by mapping the external IP address used by the browser (see the [NETWORK ARCHITECTURE](#network-architecture) section for a discussion of different types of proxies that can be used to change your external IP address). - the [languages](#languages) method, which can change the [requested languages](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) for your browser session. - the [tz](#tz) method, which can change the [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) for your browser session. This list of methods may grow. # WEBSITES THAT BLOCK AUTOMATION Marionette [by design](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/webdriver) allows web sites to detect that the browser is being automated. Firefox [no longer (since version 88)](https://bugzilla.mozilla.org/show_bug.cgi?id=1632821) allows you to disable this functionality while you are automating the browser, but this can be overridden with the `stealth` parameter for the [new](#new) method. This is extremely experimental and feedback is welcome. If the web site you are trying to automate mysteriously fails when you are automating a workflow, but it works when you perform the workflow manually, you may be dealing with a web site that is hostile to automation. I would be very interested if you can supply a test case. At the very least, under these circumstances, it would be a good idea to be aware that there's an [ongoing arms race](https://en.wikipedia.org/wiki/Web_scraping#Methods_to_prevent_web_scraping), and potential [legal issues](https://en.wikipedia.org/wiki/Web_scraping#Legal_issues) in this area. # X11 FORWARDING WITH FIREFOX [X11 Forwarding](https://man.openbsd.org/ssh#X) allows you to launch a [remote firefox via ssh](#remote-automation-of-firefox-via-ssh) and have it visually appear in your local X11 desktop. This can be accomplished with the following code; use Firefox::Marionette(); my $firefox = Firefox::Marionette->new( host => 'remote-x11.example.org', visible => 'local', debug => 1, ); $firefox->go('https://metacpan.org'); Feedback is welcome on any odd X11 workarounds that might be required for different platforms. # UBUNTU AND FIREFOX DELIVERED VIA SNAP [Ubuntu 22.04 LTS](https://ubuntu.com/blog/ubuntu-22-04-lts-whats-new-linux-desktop) is packaging firefox as a [snap](https://ubuntu.com/blog/whats-in-a-snap). This breaks the way that this module expects to be able to run, specifically, being able to setup a firefox profile in a systems temporary directory (/tmp or $TMPDIR in most Unix based systems) and allow the operating system to cleanup old directories caused by exceptions / network failures / etc. Because of this design decision, attempting to run a snap version of firefox will simply result in firefox hanging, unable to read it's custom profile directory and hence unable to read the marionette port configuration entry. Which would be workable except that; there does not appear to be \_any\_ way to detect that a snap firefox will run (/usr/bin/firefox is a bash shell which eventually runs the snap firefox), so there is no way to know (heuristics aside) if a normal firefox or a snap firefox will be launched by execing 'firefox'. It seems the only way to fix this issue (as documented in more than a few websites) is; - 1. sudo snap remove firefox - 2. sudo add-apt-repository -y ppa:mozillateam/ppa - 3. sudo apt update - 4. sudo apt install -t 'o=LP-PPA-mozillateam' firefox - 5. echo -e "Package: firefox\*\\nPin: release o=LP-PPA-mozillateam\\nPin-Priority: 501" >/tmp/mozillateamppa - 6. sudo mv /tmp/mozillateamppa /etc/apt/preferences.d/mozillateamppa If anyone is aware of a reliable method to detect if a snap firefox is going to launch vs a normal firefox, I would love to know about it. This technique is used in the [setup-for-firefox-marionette-build.sh](https://metacpan.org/pod/setup-for-firefox-marionette-build.sh) script in this distribution. # 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](#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](https://metacpan.org/pod/Firefox::Marionette::Capabilities#moz_process_id) 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](https://metacpan.org/pod/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](https://metacpan.org/pod/Firefox::Marionette::Capabilities) do not specify proxy information. # DEPENDENCIES Firefox::Marionette requires the following non-core Perl modules - [JSON](https://metacpan.org/pod/JSON) - [URI](https://metacpan.org/pod/URI) - [XML::Parser](https://metacpan.org/pod/XML::Parser) - [Time::Local](https://metacpan.org/pod/Time::Local) # 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](#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](https://metacpan.org/pod/Firefox::Marionette::Capabilities#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(); Also, check out the [download](#download) method for an alternative. ## 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](https://github.com/david-dick/firefox-marionette/issues) # SEE ALSO - [MozRepl](https://metacpan.org/pod/MozRepl) - [Selenium::Firefox](https://metacpan.org/pod/Selenium::Firefox) - [Firefox::Application](https://metacpan.org/pod/Firefox::Application) - [Mozilla::Mechanize](https://metacpan.org/pod/Mozilla::Mechanize) - [Gtk2::MozEmbed](https://metacpan.org/pod/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](http://www.softwareishard.com/blog/about/) for creating the [HAR Export Trigger](https://github.com/firefox-devtools/har-export-trigger) extension for Firefox. Thanks to [Mike Kaply](https://mike.kaply.com/about/) for his [post](https://mike.kaply.com/2015/02/10/installing-certificates-into-firefox/) describing importing certificates into Firefox. Thanks also to the authors of the documentation in the following sources; - [Marionette Protocol](https://firefox-source-docs.mozilla.org/testing/marionette/marionette/index.html) - [Marionette driver.js](https://hg.mozilla.org/mozilla-central/file/tip/remote/marionette/driver.sys.mjs) - [about:config](http://kb.mozillazine.org/About:config_entries) - [nsIPrefService interface](https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPrefService) # LICENSE AND COPYRIGHT Copyright (c) 2024, 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](https://metacpan.org/pod/perlartistic#perlartistic). The [Firefox::Marionette::Extension::HarExportTrigger](https://metacpan.org/pod/Firefox::Marionette::Extension::HarExportTrigger) module includes the [HAR Export Trigger](https://github.com/firefox-devtools/har-export-trigger) extension which is licensed under the [Mozilla Public License 2.0](https://www.mozilla.org/en-US/MPL/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.63/Changes0000644000175000017500000005277214763400566015115 0ustar davedaveRevision history for Firefox-Marionette 1.63 Mon Mar 10 07:59 2025 - Adding CLEAR_MESSAGING_LAYER_SECURITY_STATE method - geo method returns undef on failure instead of an exception - Remove ChromeUtils.import usage - Fixes for DurationFormat changes - Documentation/test suite fixes 1.62 Sun Nov 03 17:45 2024 - Adding support for socks scheme in proxy variables - Test suite fixes 1.61 Sat Oct 19 08:28 2024 - Removed CLEAR_PASSWORDS and CLEAR_SESSION_HISTORY methods - Added resolve_override method - Fixes for refactoring bookmarks API 1.60 Sun Aug 04 07:27 2024 - Added CLEAR_JS_CACHE and resolve methods - Stealth support for Object.getPrototypeOf($class).$method - Various test suite/coverage fixes 1.59 Sun Jun 30 20:10 2024 - Adding SSH_CONNECTION and EUID into syslog for ssh-auth-cmd-marionette - Changed CLEAR_PERMISSIONS, added CLEAR_SHUTDOWN_EXCEPTIONS and CLEAR_SITE_PERMISSIONS methods - Allow addons with profile names. Thanks to prozorecJP for the bug report in GH#48 - cygwin/darwin fixes for ssh-auth-cmd-marionette - Various test suite/coverage fixes 1.58 Sat Jun 15 21:01 2024 - Fixes to sftp regexs in ssh-auth-cmd-marionette - Adding initial timezone work - Fixes to stealth module - Adding CLEAR_STORAGE_PERMISSIONS and CLEAR_COOKIES_AND_SITE_DATA methods 1.57 Mon May 06 19:45 2024 - Setting minimum perl version to 5.10 1.56 Sun May 05 19:16 2024 - Using mdn/browser-compat-data for stealth - Bookmark fixes for firefox changes - Documentation fix for debian lintian 1.55 Sat Apr 06 12:22 2024 - Fixing 32 bit architecture test regex for x86 1.54 Sun Mar 31 17:43 2024 - Adding arch, set_javascript methods - Improvements for agent method - Skipping some tests on 32 bit architectures - Enabling Beacon API - Documentation and test suite fixes 1.53 Sun Mar 03 16:33 2024 - Adding CLEAR_BOUNCE_TRACKING_PROTECTION_STATE - Changes for agent method - Documentation and test suite fixes 1.52 Mon Feb 03 21:30 2024 - Adding stealth attribute - Adding CLEAR_FINGERPRINTING_PROTECTION_STATE - Supporting appVersion and platform in agent method - Documentation and test suite fixes for older perls 1.51 Mon Jan 22 21:30 2024 - Adding fixes for older perls. 1.50 Sun Jan 21 18:06 2024 - Adding agent, languages and geo methods - Allowing json method to accept a URL as a parameter - Various bug fixes, including GH#27, GH#29, GH#32, GH#33. Thanks to sergio and Pelasgus123. 1.49 Sat Nov 18 12:10 2023 - Fixing a hang for Windows 7 in GH#26. Thanks to twata1. 1.48 Mon Nov 13 20:51 2023 - Adding extra Cache constants - Fixing github actions for OS X 1.47 Sun Nov 12 22:25 2023 - Adding WebFrame and WebWindow support - Now using parent instead of base for inheritance 1.46 Sat Oct 14 18:56 2023 - Documentation and test suite fixes 1.45 Fri Oct 05 21:43 2023 - Adding WebAuthn methods - Documentation and test suite fixes 1.44 Sun Sep 03 18:04 2023 - Adding download method for directly downloading a URL - Allowing a list of proxies to be supplied to new method - Allow the all_proxy environment variable to specify an https url - Documentation and test suite fixes 1.43 Sat Aug 05 15:13 2023 - Documentation fixes 1.42 Sat Aug 05 08:34 2023 - Adding the proxy parameter to the new method - Fixes to ssh handling - Documentation and test suite fixes - Allowing a scalar filehandle to contain a certificate for the new method 1.41 Sun Jul 23 17:12 2023 - Adding support for finding elements in a shadow DOM 1.40 Wed Jul 19 04:14 2023 - Documentation and test suite fixes 1.39 Tue Jul 18 18:35 2023 - Adding bookmark support - Adding tls parameter to Proxy->new method - Adding aria_label and aria_role methods - Documentation and test suite fixes 1.38 Sun May 28 06:57 2023 - Fixing ssh-auth-cmd-marionette for FreeBSD in GH#24. Thanks to prozorecJP - Adding cache_keys, check_cache_key and clear_cache methods - Renaming download method to downloaded - Test coverage improvements and related fixes 1.37 Sun Apr 30 19:12 2023 - Another test suite fix 1.36 Sun Apr 30 15:59 2023 - Fixes to cope with Firefox 112 capabilities changes. Thanks to toreau - Documentation and test suite fixes 1.35 Sat Jan 21 21:04 2023 - Documentation and test suite fixes 1.34 Fri Dec 30 12:52 2022 - Adding percentage_visible, displays and resize methods - Documentation and test suite fixes 1.33 Sun Nov 27 21:38 2022 - Improving cross platform support - Forcing no agent forwarding for ssh - Trying to use the DISPLAY variable to detect X11 Forwarding being disabled 1.32 Tue Nov 08 19:42 2022 - Win32 test suite fixes 1.31 Mon Nov 07 20:43 2022 - Fixing taint test suite failures in github actions 1.30 Mon Nov 07 16:26 2022 - Fixing test suite for FreeBSD by adding /usr/local/bin to PATH 1.29 Sun Nov 06 15:55 2022 - Adding wheel, scroll, is_trusted, uname method - Adding support for remotely opening profiles by name - Adding --issuer-organization, --trusted-only and --profile-name options to ./ca-bundle-for-firefox - Hiding bookmarks toolbar 1.28 Mon Jun 13 11:54 2022 - Adding ./setup-for-firefox-marionette-build.sh to remove the snap firefox for Ubuntu 22.04 TLS. 1.27 Sun May 08 21:38 2022 - Fixing missing META information 1.26 Sun May 01 06:31 2022 - Adding support for about:config prefs.js dynamic changes 1.25 Mon Apr 25 09:32 2022 - Adding X11 Forwarding and support for ssh jump hosts - Reduce network and disk load during 'make test' - Improving test suite (coverage now > 90%) 1.24 Fri Apr 8 19:32 2022 - Fixes for CPAN Testers results - Adding visible support for remote Firefox instances on linux (via xvfb-run) - Test suite changes for darwin 1.23 Sat Apr 2 21:14 2022 - Fixes to tests, cygwin, Win32 support - Adding documentation/tests for WebGL - Improvements to ssh-auth-cmd-marionette - Parameter cleanup for new browser versions - Adding logins_from_xml method, --show-next to check-firefox-certificate-authorities 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.63/LICENSE0000644000175000017500000004702514763400566014622 0ustar davedaveThis is free software; you can redistribute it and/or modify it under the same terms as the Perl5 (v5.0.0 ~ v5.40.0) programming language system itself: under the terms of either: a) the "Artistic License 1.0" as published by The Perl Foundation https://www.perlfoundation.org/artistic-license-10.html 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.63/mozilla-head-check0000755000175000017500000001416614763400572017161 0ustar davedave#! /usr/bin/perl use strict; use warnings; use Cwd(); use File::Temp(); use File::Spec(); use FileHandle(); use English qw( -no_match_vars ); use Carp(); use DirHandle(); use Sys::Syslog(); our $VERSION = '1.63'; MAIN: { my $facility = 'LOG_LOCAL0'; my $absolute_program_name; if ( File::Spec->file_name_is_absolute($PROGRAM_NAME) ) { $absolute_program_name = $PROGRAM_NAME; } else { $absolute_program_name = File::Spec->catfile( Cwd::cwd(), $PROGRAM_NAME ); } my ( $original_volume, $original_directories, $original_name ) = File::Spec->splitpath($absolute_program_name); my $ident = $original_name; my $base_directory = File::Spec->catdir( $original_volume, $original_directories ); my $mozilla_central_directory = File::Spec->canonpath( File::Spec->catdir( $base_directory, q[..], 'mozilla-central' ) ); build_firefox($mozilla_central_directory); my $changeset = most_recent_firefox_changeset($mozilla_central_directory); my $firefox_path = File::Spec->catfile( $mozilla_central_directory, 'obj-x86_64-pc-linux-gnu', 'dist', 'bin', 'firefox' ); local $ENV{SSH_AUTH_SOCK} = get_ssh_auth_sock(); local $ENV{SSH_AUTH_SOCK} = get_ssh_auth_sock(); local $ENV{FIREFOX_BINARY} = $firefox_path; local $ENV{RELEASE_TESTING} = 1; system {$EXECUTABLE_NAME} $EXECUTABLE_NAME, 'Makefile.PL' and Carp::croak(q[Failed to 'perl Makefile.PL']); system {'cover'} 'cover', '-test' and Carp::croak(q[Failed to 'cover -test']); system {'make'} 'make', 'clean' and Carp::croak(q[Failed to 'make clean']); system {'rm'} 'rm', 'Makefile.old' and Carp::croak(q[Failed to 'rm Makefile.old']); Sys::Syslog::openlog( $ident, 'cons', $facility ); Sys::Syslog::syslog( Sys::Syslog::LOG_INFO(), "Passed cover -test at mozilla-central changeset $changeset" ); Sys::Syslog::closelog(); } sub get_ssh_auth_sock { if ( $ENV{SSH_AUTH_SOCK1} ) { return $ENV{SSH_AUTH_SOCK}; } else { my $tmp_directory = File::Spec->tmpdir(); my $tmp_handle = DirHandle->new($tmp_directory) or Carp::croak( "Failed to open directory $tmp_directory:$EXTENDED_OS_ERROR"); while ( my $agent_entry = $tmp_handle->read() ) { if ( $agent_entry =~ /^(ssh\-[[:alnum:]]+)$/smx ) { my $ssh_agent_directory = File::Spec->catfile( $tmp_directory, $1 ); if ( my $agent_file = find_ssh_agent_file($ssh_agent_directory) ) { return $agent_file; } } } } return; } sub find_ssh_agent_file { my ($ssh_agent_directory) = @_; my $ssh_agent_handle = DirHandle->new($ssh_agent_directory) or Carp::croak( "Failed to open directory $ssh_agent_directory:$EXTENDED_OS_ERROR"); while ( my $pid_entry = $ssh_agent_handle->read() ) { if ( $pid_entry =~ /^agent[.](\d+)$/smx ) { my $ppid = $1; if ( kill 0, $ppid ) { return File::Spec->catfile( $ssh_agent_directory, 'agent.' . $ppid ); } } } return; } sub build_firefox { my ($mozilla_central_directory) = @_; my ( $mozilla_volume, $mozilla_directories, $mozilla_name ) = File::Spec->splitpath($mozilla_central_directory); my $mozilla_parent_directory = File::Spec->catdir( $mozilla_volume, $mozilla_directories ); if ( my $pid = fork ) { waitpid $pid, 0; if ( $CHILD_ERROR != 0 ) { Carp::croak("Failed to checkout on $mozilla_central_directory"); } } elsif ( defined $pid ) { eval { if ( !-d $mozilla_central_directory ) { chdir $mozilla_parent_directory or Carp::croak( "Failed to chdir $mozilla_parent_directory:$EXTENDED_OS_ERROR" ); system {'hg'} 'hg', 'clone', 'https://hg.mozilla.org/mozilla-central/', $mozilla_name and Carp::croak( "Failed to 'hg clone https://hg.mozilla.org/mozilla-central/ $mozilla_name:$EXTENDED_OS_ERROR" ); } chdir $mozilla_central_directory or Carp::croak( "Failed to chdir $mozilla_central_directory:$EXTENDED_OS_ERROR" ); system {'hg'} 'hg', 'pull' and Carp::croak("Failed to 'hg pull' on $mozilla_central_directory"); system {'hg'} 'hg', 'update', '--clean' and Carp::croak( "Failed to 'hg update --clean' on $mozilla_central_directory"); system {'./mach'} './mach', 'clobber' and Carp::croak( "Failed to './mach clobber' on $mozilla_central_directory"); system {'./mach'} './mach', 'build' and Carp::croak( "Failed to './mach build' on $mozilla_central_directory"); exit 0; } or do { chomp $EVAL_ERROR; Carp::carp($EVAL_ERROR); }; exit 1; } else { Carp::croak("Failed to fork:$OS_ERROR"); } return; } sub most_recent_firefox_changeset { my ($mozilla_central_directory) = @_; my $handle = FileHandle->new(); my $changeset; if ( my $pid = $handle->open(q[-|]) ) { while ( my $line = <$handle> ) { if ( $line =~ /^changeset:[ ]+(\d+:[[:xdigit:]]+)\s*$/smx ) { ($changeset) = ($1); } } close $handle or Carp::croak(q[Failed to successfully run 'hg heads']); } elsif ( defined $pid ) { eval { chdir $mozilla_central_directory or Carp::croak( "Failed to chdir $mozilla_central_directory:$EXTENDED_OS_ERROR" ); exec {'hg'} 'hg', 'heads' or Carp::croak("Failed to exec 'hg':$EXTENDED_OS_ERROR"); } or do { chomp $EVAL_ERROR; Carp::carp($EVAL_ERROR); }; exit 1; } else { Carp::croak("Failed to fork:$OS_ERROR"); } return $changeset; } Firefox-Marionette-1.63/ca-bundle-for-firefox0000755000175000017500000001654314763400566017622 0ustar davedave#! /usr/bin/perl use strict; use warnings; use Getopt::Long qw(:config bundling); use English qw( -no_match_vars ); use Firefox::Marionette(); use FileHandle(); use Encode(); our $VERSION = '1.63'; my %options; MAIN: { Getopt::Long::GetOptions( \%options, 'help|h', 'version|v', 'binary|b:s', 'file|f:s', 'issuer-organization|i', 'trusted-only|t', 'profile-name|p:s', 'default-profile|d', ); my %parameters = _validate_options_and_parse_parameters(); 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}; } my %sort_name; foreach my $certificate ( sort { ( $sort_name{$a} ||= $options{'issuer-organization'} ? $a->issuer_organization() || $a->display_name() : $a->display_name() ) cmp( $sort_name{$b} ||= $options{'issuer-organization'} ? $b->issuer_organization() || $b->display_name() : $b->display_name() ) } $firefox->certificates() ) { if ( $certificate->is_ca_cert() ) { if ( ( $options{'trusted-only'} ) && ( !$firefox->is_trusted($certificate) ) ) { next; } my $output_line = q[# ] . ( $options{'issuer-organization'} ? ( $certificate->issuer_organization() || $certificate->display_name() ) . q[ -- ] : 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"; } } sub _validate_options_and_parse_parameters { 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 %mapping = ( 'profile-name' => 'profile_name', binary => 'binary', ); my %parameters; foreach my $key_name (qw( binary profile-name )) { if ( defined $options{$key_name} ) { $parameters{ $mapping{$key_name} } = $options{$key_name}; } } if ( ( $parameters{profile_name} ) || ( $options{'default-profile'} ) ) { my $profile_name = delete $parameters{profile_name} || Firefox::Marionette::Profile->default_name(); my $profile_directory = Firefox::Marionette::Profile->directory($profile_name); my $cert_db_path = File::Spec->catfile( $profile_directory, 'cert9.db' ); $parameters{import_profile_paths} = [$cert_db_path]; } return %parameters; } __END__ =head1 NAME ca-bundle-for-firefox - generate the ca-bundle.crt for the current firefox instance =head1 VERSION Version 1.63 =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 $ ca-bundle-for-firefox --issuer-organization --trusted-only --profile-name default $ ca-bundle-for-firefox --issuer-organization --trusted-only --default-profile $ ca-bundle-for-firefox -dit =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 * --default-profile - Use the certificate database from the default profile =item * --issuer-organization - Print the Issuer Organisation as well as the Display Name in the comment line. The Issuer Organisation is used in Firefox's Certificate Manager. =item * --trusted-only - Only output certificates that are trusted in the current profile =item * --profile-name - Use the certificate database for a particular profile =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) 2024, 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.63/META.json0000644000175000017500000000535314763402247015232 0ustar davedave{ "abstract" : "Automate the Firefox browser with the Marionette protocol", "author" : [ "David Dick " ], "dynamic_config" : 1, "generated_by" : "ExtUtils::MakeMaker version 7.70, 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", "Crypt::PasswdMD5" : "0", "Cwd" : "0", "Digest::SHA" : "0", "File::HomeDir" : "0", "HTTP::Daemon" : "0", "HTTP::Response" : "0", "HTTP::Status" : "0", "IO::Socket::IP" : "0", "IO::Socket::SSL" : "0", "PDF::API2" : "2.036", "Test::CheckManifest" : "0.9", "Test::More" : "0", "Test::Pod" : "1.41", "Test::Pod::Coverage" : "1.04" } }, "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", "IO::Handle" : "0", "IPC::Open3" : "1.03", "JSON" : "0", "MIME::Base64" : "3.11", "POSIX" : "0", "Pod::Simple::Text" : "0", "Scalar::Util" : "0", "Socket" : "0", "Term::ReadKey" : "0", "Text::CSV_XS" : "1.35", "Time::HiRes" : "0", "Time::Local" : "0", "URI" : "1.61", "URI::Escape" : "0", "URI::URL" : "0", "URI::data" : "0", "XML::Parser" : "0", "overload" : "0", "parent" : "0", "perl" : "5.010" } } }, "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.63", "x_serialization_backend" : "JSON::PP version 4.16" } Firefox-Marionette-1.63/setup-for-firefox-marionette-build.sh0000755000175000017500000002714314763400572022776 0ustar davedave#! /bin/sh SUDO="sudo "; SUDO_WITH_ENVIRONMENT="$SUDO -E "; OSNAME=`uname`; if [ ! $EUID ] then EUID=`id -u`; fi if [ $EUID -eq 0 ] then SUDO=""; SUDO_WITH_ENVIRONMENT=""; fi case $OSNAME in Linux) if [ -e "/etc/redhat-release" ] then DNF="dnf" $DNF --help >/dev/null 2>/dev/null || DNF="yum" if [ ! -e "/etc/fedora-release" ] then rpm -q --quiet epel-release 2>/dev/null && ${SUDO}$DNF install -y epel-release || true fi PACKAGES="dbus-x11 \ firefox \ git \ make \ mesa-dri-drivers \ nginx \ openssl \ perl-Archive-Zip \ perl-Crypt-PasswdMD5 \ perl-Crypt-URandom \ perl-Digest-SHA \ perl-ExtUtils-MakeMaker \ perl-File-HomeDir \ perl-Font-TTF \ perl-HTTP-Daemon \ perl-HTTP-Message \ perl-IO-Socket-SSL \ perl-JSON \ perl-PerlIO-utf8_strict \ perl-Sub-Exporter \ perl-Sub-Uplevel \ perl-Text-CSV_XS \ perl-TermReadKey \ perl-Test-Exception \ perl-Test-Memory-Cycle \ perl-Test-Simple \ perl-XML-Parser \ squid \ xorg-x11-server-Xvfb \ yarnpkg" rpm -q --quiet $PACKAGES || ${SUDO}$DNF install -y $PACKAGES SOMETIMES_MISSING_PACKAGES="perl-Config-INI \ perl-DirHandle \ perl-PDF-API2" for PACKAGE in $SOMETIMES_MISSING_PACKAGES do rpm -q --quiet $PACKAGE || ${SUDO}$DNF install -y $PACKAGE done for PACKAGE in Config::INI PDF::API2 do perl -M$PACKAGE -e 'exit 0' if [ $? != 0 ] then ${SUDO}dnf install -y cpan PERL_MM_USE_DEFAULT=1 ${SUDO_WITH_ENVIRONMENT}cpan $PACKAGE fi done fi if [ -e "/etc/SUSE-brand" ] then PACKAGES="dbus-1-x11 \ MozillaFirefox \ git \ make \ Mesa-dri-nouveau \ nginx \ openssl \ perl-Archive-Zip \ perl-Config-INI \ perl-Crypt-PasswdMD5 \ perl-Crypt-URandom \ perl-ExtUtils-MakeMaker \ perl-File-HomeDir \ perl-Font-TTF \ perl-HTTP-Daemon \ perl-HTTP-Message \ perl-IO-Socket-SSL \ perl-JSON \ perl-PDF-API2 \ perl-PerlIO-utf8_strict \ perl-Sub-Exporter \ perl-Sub-Uplevel \ perl-Text-CSV_XS \ perl-Term-ReadKey \ perl-Test-Exception \ perl-Test-Memory-Cycle \ perl-Test-Simple \ perl-XML-Parser \ squid \ xorg-x11-server-Xvfb \ yarn" rpm -q --quiet $PACKAGES || ${SUDO}zypper install -y $PACKAGES fi if [ -e "/etc/debian_version" ] then ${SUDO}apt-get update DEBIAN_FIREFOX_PACKAGE_NAME="firefox" if [ ! -e /etc/apt/preferences.d/mozillateamppa ] then NUMBER_OF_FIREFOX_PACKAGES=`apt-cache search '^firefox$' | wc -l` if [ $NUMBER_OF_FIREFOX_PACKAGES -eq 0 ] then # debian only has a firefox-esr package DEBIAN_FIREFOX_PACKAGE_NAME="firefox-esr" fi fi ${SUDO}apt-get install -y $DEBIAN_FIREFOX_PACKAGE_NAME SNAP_FOUND=1 SNAP_FIREFOX_FOUND=0 snap list $DEBIAN_FIREFOX_PACKAGE_NAME 2>/dev/null && SNAP_FIREFOX_FOUND=1 || SNAP=0 if [ $SNAP_FOUND -eq 1 ] && [ $SNAP_FIREFOX_FOUND -eq 1 ] then ${SUDO}snap remove $DEBIAN_FIREFOX_PACKAGE_NAME ${SUDO}apt-get remove -y $DEBIAN_FIREFOX_PACKAGE_NAME ${SUDO}add-apt-repository -y ppa:mozillateam/ppa ${SUDO}apt-get update ${SUDO}apt-get install -y -t 'o=LP-PPA-mozillateam' firefox TMP_MOZILLA_REPO_DIR=`mktemp -d` /usr/bin/echo -e "Package: firefox*\nPin: release o=LP-PPA-mozillateam\nPin-Priority: 501" >$TMP_MOZILLA_REPO_DIR/mozillateamppa ${SUDO}mv $TMP_MOZILLA_REPO_DIR/mozillateamppa /etc/apt/preferences.d/mozillateamppa rmdir $TMP_MOZILLA_REPO_DIR fi ${SUDO}apt-get install -y \ dbus-x11 \ git \ libarchive-zip-perl \ libconfig-ini-perl \ libcrypt-urandom-perl \ libcrypt-passwdmd5-perl \ libfile-homedir-perl \ libhttp-daemon-perl \ libhttp-message-perl \ libio-socket-ssl-perl \ libjson-perl \ libpdf-api2-perl \ libpod-parser-perl \ libtest-checkmanifest-perl \ libtest-pod-coverage-perl \ libtext-csv-xs-perl \ libterm-readkey-perl \ liburi-perl \ libxml-parser-perl \ make \ nginx \ openssh-server \ squid \ xvfb \ yarnpkg fi if [ -e "/etc/alpine-release" ] then PACKAGES="dbus-x11 \ firefox \ git \ mesa-dri-nouveau \ nginx \ openssl \ perl \ perl-archive-zip \ perl-config-ini \ perl-crypt-passwdmd5 \ perl-crypt-urandom \ perl-file-homedir \ perl-http-daemon \ perl-http-message \ perl-io-socket-ssl \ perl-json \ perl-pdf-api2 \ perl-term-readkey \ perl-test-pod \ perl-test-pod-coverage \ perl-test-simple \ perl-text-csv_xs \ perl-uri \ perl-xml-parser \ make \ squid \ xauth \ xvfb \ yarn" ${SUDO}apk update ${SUDO}apk upgrade INSTALL_PACKAGES=0 for PACKAGE_NAME in $PACKAGES do grep $PACKAGE_NAME /etc/apk/world >/dev/null || INSTALL_PACKAGES=1 done if [ $INSTALL_PACKAGES -eq 1 ] then ${SUDO}apk add $PACKAGES if [ $? != 0 ] then cat <<"_APK_REPO_"; Check the /etc/apk/repositories file as it needs to have the community and main repos uncommented and probably the edge repositories as well, like so; #/media/cdrom/apks http://dl-cdn.alpinelinux.org/alpine/v3.16/main http://dl-cdn.alpinelinux.org/alpine/v3.16/community http://dl-cdn.alpinelinux.org/alpine/edge/main http://dl-cdn.alpinelinux.org/alpine/edge/community http://dl-cdn.alpinelinux.org/alpine/edge/testing _APK_REPO_ fi fi fi ;; DragonFly) PACKAGES="firefox \ git \ mesa-dri-gallium \ nginx \ openssl \ perl5 \ p5-Archive-Zip \ p5-JSON \ p5-Config-INI \ p5-Crypt-PasswdMD5 \ p5-Crypt-URandom \ p5-File-HomeDir \ p5-Digest-SHA \ p5-HTTP-Daemon \ p5-HTTP-Message \ p5-IO-Socket-SSL \ p5-PDF-API2 \ p5-Text-CSV_XS \ p5-Term-ReadKey \ p5-Test-CheckManifest \ p5-Test-Pod \ p5-Test-Pod-Coverage \ p5-Test-Simple \ p5-XML-Parser \ squid \ xauth \ xorg-vfbserver \ yarn" ${SUDO}pkg upgrade -y pkg info $PACKAGES >/dev/null || ${SUDO}pkg install -y $PACKAGES if [ ! -e /etc/machine-id ] then ${SUDO}dbus-uuidgen --ensure=/etc/machine-id fi ;; FreeBSD) PACKAGES="firefox-esr \ git \ nginx \ openssl \ perl5 \ p5-Archive-Zip \ p5-JSON \ p5-Config-INI \ p5-Crypt-PasswdMD5 \ p5-Crypt-URandom \ p5-File-HomeDir \ p5-Digest-SHA \ p5-HTTP-Daemon \ p5-HTTP-Message \ p5-IO-Socket-SSL \ p5-PDF-API2 \ p5-Text-CSV_XS \ p5-Term-ReadKey \ p5-Test-CheckManifest \ p5-Test-Pod \ p5-Test-Pod-Coverage \ p5-Test-Simple \ p5-XML-Parser \ squid \ xauth \ xorg-vfbserver \ yarn" ${SUDO}pkg upgrade -y pkg info $PACKAGES >/dev/null || ${SUDO}pkg install -y $PACKAGES mount | grep fdescfs >/dev/null || ${SUDO}mount -t fdescfs fdesc /dev/fd if [ ! -e /etc/machine-id ] then ${SUDO}dbus-uuidgen --ensure=/etc/machine-id fi ;; MidnightBSD) PACKAGES="firefox \ git \ nginx \ openssl \ perl5 \ p5-Archive-Zip \ p5-JSON \ p5-Config-INI \ p5-Crypt-PasswdMD5 \ p5-Crypt-URandom \ p5-File-HomeDir \ p5-Digest-SHA \ p5-HTTP-Daemon \ p5-HTTP-Message \ p5-IO-Socket-SSL \ p5-PDF-API2 \ p5-Text-CSV_XS \ p5-Term-ReadKey \ p5-Test-CheckManifest \ p5-Test-Pod \ p5-Test-Pod-Coverage \ p5-Test-Simple \ p5-XML-Parser \ squid \ xauth \ xorg-vfbserver \ yarn" for NAME in $PACKAGES do mport info $NAME >/dev/null || ${SUDO}mport install $NAME done mount | grep fdescfs >/dev/null || ${SUDO}mount -t fdescfs fdesc /dev/fd if [ ! -e /etc/machine-id ] then ${SUDO}dbus-uuidgen --ensure=/etc/machine-id fi ;; OpenBSD) PACKAGES="firefox \ git \ nginx \ p5-Archive-Zip \ p5-JSON \ p5-Config-INI \ p5-Crypt-PasswdMD5 \ p5-Crypt-URandom \ p5-File-HomeDir \ p5-HTTP-Daemon \ p5-HTTP-Message \ p5-IO-Socket-SSL \ p5-Params-Util \ p5-PerlIO-utf8_strict \ p5-PDF-API2 \ p5-Sub-Exporter \ p5-Sub-Uplevel \ p5-Sub-Install \ p5-Test-CheckManifest \ p5-Test-Pod-Coverage \ p5-Text-CSV_XS \ p5-XML-Parser \ squid \ yarn" ${SUDO}pkg_add -u pkg_info $PACKAGES >/dev/null || ${SUDO}pkg_add -I $PACKAGES perl -MConfig::INI -e 'exit 0' || PERL_MM_USE_DEFAULT=1 ${SUDO_WITH_ENVIRONMENT} cpan Config::INI perl -MCrypt::URandom -e 'exit 0' || PERL_MM_USE_DEFAULT=1 ${SUDO_WITH_ENVIRONMENT} cpan Crypt::URandom ;; NetBSD) PKG_PATH="http://cdn.NetBSD.org/pub/pkgsrc/packages/NetBSD/$(uname -p)/$(uname -r|cut -f '1 2' -d.)/All/" export PKG_PATH ${SUDO}pkg_add pkgin PACKAGES="firefox \ git \ mozilla-rootcerts-openssl \ nginx \ openssl \ p5-Archive-Zip \ p5-JSON \ p5-Config-INI \ p5-Crypt-PasswdMD5 \ p5-Crypt-URandom \ p5-File-HomeDir \ p5-HTTP-Daemon \ p5-HTTP-Message \ p5-IO-Socket-SSL \ p5-Params-Util \ p5-PerlIO-utf8_strict \ p5-PDF-API2 \ p5-Sub-Exporter \ p5-Sub-Uplevel \ p5-Sub-Install \ p5-Text-CSV_XS \ p5-Term-ReadKey \ p5-Test-CheckManifest \ p5-Test-Pod \ p5-Test-Pod-Coverage \ p5-XML-Parser \ squid \ yarn" INSTALL_PACKAGES="" ${SUDO}pkgin upgrade for NAME in $PACKAGES do pkgin list | grep $NAME >/dev/null 2>/dev/null || INSTALL_PACKAGES="$INSTALL_PACKAGES $NAME" done if [ "$INSTALL_PACKAGES" != "" ] then ${SUDO}pkg_add ${PKG_PATH}/pkgin ${SUDO}pkgin -y install $INSTALL_PACKAGES if [ $? != 0 ] then cat <<_PKG_PATH_ pkg_add failed. PKG_PATH was set to $PKG_PATH _PKG_PATH_ fi fi ;; CYGWIN_NT*) PACKAGE_LIST="perl-Archive-Zip" for PACKAGE in perl-JSON \ perl-Config-INI \ perl-Crypt-PasswdMD5 \ perl-Crypt-URandom \ perl-File-HomeDir \ perl-Digest-SHA \ perl-HTTP-Daemon \ perl-HTTP-Message \ perl-IO-Socket-SSL \ perl-PDF-API2 \ perl-PerlIO-utf8_strict \ perl-Sub-Exporter \ perl-Sub-Uplevel \ perl-Text-CSV_XS \ perl-Term-ReadKey \ perl-Test-Simple \ perl-XML-Parser do PACKAGE_LIST="$PACKAGE_LIST,$PACKAGE" done GUESS_EXE=`cygpath -u $USERPROFILE`/Downloads/setup-x86_64.exe grep -ail 'Cygwin installation tool' $GUESS_EXE if [ $? == 0 ] then SETUP_EXE=$GUESS_EXE else SETUP_EXE=`find /cygdrive -name 'setup-x86_64.exe' -exec grep -ail 'Cygwin installation tool' {} \;` fi $SETUP_EXE -q -P $PACKAGE_LIST perl -MConfig::INI -e 'exit 0' || PERL_MM_USE_DEFAULT=1 cpan Config::INI perl -MPDF::API2 -e 'exit 0' || PERL_MM_USE_DEFAULT=1 cpan PDF::API2 perl -MCrypt::URandom -e 'exit 0' || PERL_MM_USE_DEFAULT=1 cpan Crypt::URandom perl -MCrypt::PasswdMD5 -e 'exit 0' || PERL_MM_USE_DEFAULT=1 cpan Crypt::PasswdMD5 ;; Darwin) for PACKAGE in Archive::Zip \ JSON \ Config::INI \ Crypt::PasswdMD5 \ Crypt::URandom \ File::HomeDir \ Digest::SHA \ HTTP::Daemon \ HTTP::Message \ IO::Socket::SSL \ PDF::API2 \ PerlIO::utf8_strict \ Sub::Exporter \ Sub::Uplevel \ Text::CSV_XS \ Term::ReadKey \ Test::CheckManifest \ Test::Pod \ Test::Pod::Coverage \ Test::Simple \ XML::Parser do perl -M$PACKAGE -e 'exit 0' 2>>/dev/null || PERL_MM_USE_DEFAULT=1 ${SUDO_WITH_ENVIRONMENT}cpan $PACKAGE done ;; *) echo "Any help with patching '$OSNAME' support would be awesome???" ;; esac Firefox-Marionette-1.63/MANIFEST0000644000175000017500000000526314763400566014744 0ustar davedaveChanges lib/Firefox/Marionette.pm lib/Firefox/Marionette/Bookmark.pm lib/Firefox/Marionette/Buttons.pm lib/Firefox/Marionette/Cache.pm lib/Firefox/Marionette/Capabilities.pm lib/Firefox/Marionette/Certificate.pm lib/Firefox/Marionette/Cookie.pm lib/Firefox/Marionette/DNS.pm lib/Firefox/Marionette/Extension/HarExportTrigger.pm lib/Firefox/Marionette/Extension/Stealth.pm lib/Firefox/Marionette/Extension/Timezone.pm lib/Firefox/Marionette/Image.pm lib/Firefox/Marionette/Keys.pm lib/Firefox/Marionette/Link.pm lib/Firefox/Marionette/LocalObject.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/Display.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/GeoLocation.pm lib/Firefox/Marionette/Response.pm lib/Firefox/Marionette/Timeouts.pm lib/Firefox/Marionette/UpdateStatus.pm lib/Firefox/Marionette/WebAuthn/Authenticator.pm lib/Firefox/Marionette/WebAuthn/Credential.pm lib/Firefox/Marionette/WebFrame.pm lib/Firefox/Marionette/WebWindow.pm lib/Waterfox/Marionette.pm lib/Waterfox/Marionette/Profile.pm Makefile.PL MANIFEST LICENSE README README.md SECURITY.md build-bcd-for-firefox ca-bundle-for-firefox check-firefox-certificate-authorities mozilla-head-check setup-for-firefox-marionette-build.sh ssh-auth-cmd-marionette firefox-passwords t/author/bulk_test.pl t/data/bookmarks_chrome.html t/data/bookmarks_edge.html t/data/bookmarks_empty.html t/data/bookmarks_firefox.html t/data/bookmarks_firefox.json t/data/bookmarks_truncated.html t/data/aria.html t/data/elements.html t/data/iframe.html t/data/timezone.html t/data/visible.html t/data/logins.json t/data/keepass1.xml t/data/keepassxs.csv 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/02-taint.t t/03-close.t t/03-closedir.t t/03-fork.t t/03-mkdir.t t/03-opendir.t t/03-read.t t/03-seek.t t/03-stat.t t/03-sysopen.t t/04-botd.t t/04-browserfeatcl.t t/04-fingerprint.t t/04-proxy.t t/04-timezone.t t/04-webauthn.t t/manifest.t t/pod-coverage.t t/pod.t t/addons/test.xpi t/addons/borderify/borderify.js t/addons/borderify/manifest.json t/addons/discogs-search/manifest.json t/addons/discogs-search/README.md t/stub.pl t/syscall_tests.pm t/test_daemons.pm META.yml META.json Firefox-Marionette-1.63/Makefile.PL0000644000175000017500000003565214616127346015571 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"; } foreach my $env_name ( 'XAUTHORITY', # see GH#1 'XDG_RUNTIME_DIR', # see GH#33 ) { if ( exists $ENV{$env_name} ) { delete $ENV{$env_name}; warn "Running as root. Deleting the $env_name 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) { my $output = `"$possible.exe" -version`; if ($output) { warn "Version of $possible in Path environment variable:$output"; } else { warn "No $possible in Path environment variable"; } } } elsif ( ( $OSNAME eq 'darwin' ) or ( $OSNAME eq 'cygwin' ) ) { foreach my $possible (@possibles) { my $output = `$possible -version`; if ($output) { warn "Version of $possible in PATH environment variable:$output"; } else { warn "No $possible in PATH environment variable"; } } } 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' ) { PATH: 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; last 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 ) { if ( $OSNAME eq 'linux' ) { foreach my $line ( split /\r?\n/smx, `snap list firefox 2>/dev/null` ) { if ( $line =~ /^firefox[ ]+/smx ) { die "We found a snapped firefox. This module does not work with snap. The ./setup-for-firefox-marionette-build.sh script contains an alternative way to setup firefox that works with this module\n"; } } } 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.010' ) : () ), 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', 'setup-for-firefox-marionette-build.sh', ], BUILD_REQUIRES => { 'Compress::Zlib' => 0, 'Crypt::PasswdMD5' => 0, 'Cwd' => 0, 'Digest::SHA' => 0, 'File::HomeDir' => 0, 'HTTP::Daemon' => 0, 'HTTP::Response' => 0, 'HTTP::Status' => 0, 'IO::Socket::IP' => 0, 'IO::Socket::SSL' => 0, $] ge '5.010' ? ( 'PDF::API2' => 2.036 ) : (), ( $OSNAME eq 'cygwin' ? () : ( 'Test::CheckManifest' => 0.9 ) ), 'Test::More' => 0, 'Test::Pod::Coverage' => 1.04, 'Test::Pod' => 1.41, }, PREREQ_PM => { 'Archive::Zip' => 0, 'Config' => 0, 'Config::INI::Reader' => 0, 'Crypt::URandom' => 0, 'DirHandle' => 0, 'Encode' => 0, 'English' => 0, 'Exporter' => 0, 'Fcntl' => 0, 'FileHandle' => 0, 'File::Find' => 0, 'File::Spec' => 0, 'File::Temp' => 0, 'IO::Handle' => 0, 'IPC::Open3' => 1.03, 'JSON' => 0, 'MIME::Base64' => 3.11, # encode_base64url 'overload' => 0, 'parent' => 0, 'Pod::Simple::Text' => 0, 'POSIX' => 0, 'Scalar::Util' => 0, 'Socket' => 0, 'Text::CSV_XS' => 1.35, 'Time::Local' => 0, 'Term::ReadKey' => 0, 'Time::HiRes' => 0, 'URI' => 1.61, 'URI::Escape' => 0, 'URI::data' => 0, 'URI::URL' => 0, ( $OSNAME eq 'MSWin32' ? ( 'Win32' => 0, 'Win32::Process' => 0, 'Win32API::Registry' => 0, ) : () ), 'XML::Parser' => 0, }, dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, clean => { FILES => 'Firefox-Marionette-*' }, ); Firefox-Marionette-1.63/firefox-passwords0000755000175000017500000004371014763400572017222 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 XML::Parser(); use Term::ReadKey(); use charnames ':full'; our $VERSION = '1.63'; 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); } } open my $duplicate_handle, q[>&], $import_handle or die "Failed to duplicate import handle:$EXTENDED_OS_ERROR\n"; my $parser = XML::Parser->new(); my $xml = 1; eval { $parser->parse($duplicate_handle); } or do { $xml = 0; }; close $duplicate_handle or die "Failed to close temporary handle:$EXTENDED_OS_ERROR\n"; if ($xml) { sysseek $import_handle, Fcntl::SEEK_SET(), 0 or die "Failed to seek to start of file:$EXTENDED_OS_ERROR\n"; return Firefox::Marionette->logins_from_xml($import_handle); } else { 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.63 =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 =item * L =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) 2024, 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.63/SECURITY.md0000644000175000017500000000707614763400566015410 0ustar davedaveThis is the Security Policy for the Perl distribution Firefox-Marionette. The latest version of this Security Policy can be found in the git repository at [https://github.com/david-dick/firefox-marionette/security/policy](https://github.com/david-dick/firefox-marionette/security/policy). This text is based on the CPAN Security Group's Guidelines for Adding a Security Policy to Perl Distributions (version 0.1.9). https://security.metacpan.org/docs/guides/security-policy-for-authors.html # How to Report a Security Vulnerability Security vulnerabilties can be reported by e-mail to the current project maintainer(s) at . Please include as many details as possible, including code samples or test cases, so that we can reproduce the issue. If you would like any help with triaging the issue, or if the issue is being actively exploited, please copy the report to the CPAN Security Group (CPANSec) at . Please *do not* use the public issue reporting system on RT or GitHub issues for reporting security vulnerabilities. Please do not disclose the security vulnerability in public forums until past any proposed date for public disclosure, or it has been made public by the maintainers or CPANSec. That includes patches or pull requests. For more information, see [Report a Security Issue](https://security.metacpan.org/docs/report.html) on the CPANSec website. ## Response to Reports The maintainer(s) aim to acknowledge your security report as soon as possible. However, this project is maintained by a single person in their spare time, and they cannot guarantee a rapid response. If you have not received a response from the them within a week, then please send a reminder to them and copy the report to CPANSec at . Please note that the initial response to your report will be an acknowledgement, with a possible query for more information. It will not necessarily include any fixes for the issue. The project maintainer(s) may forward this issue to the security contacts for other projects where we believe it is relevant. This may include embedded libraries, system libraries, prerequisite modules or downstream software that uses this software. They may also forward this issue to CPANSec. # What Software this Policy Applies to Any security vulnerabilities in Firefox-Marionette are covered by this policy. Security vulnerabilities are considered anything that allows users to execute unauthorised code, access unauthorised resources, or to have an adverse impact on accessibility or performance of a system. Security vulnerabilities in upstream software (embedded libraries, prerequisite modules or system libraries, or in Perl), are not covered by this policy unless they affect Firefox-Marionette, or Firefox-Marionette can be used to exploit vulnerabilities in them. Security vulnerabilities in downstream software (any software that uses Firefox-Marionette, or plugins to it that are not included with the Firefox-Marionette distribution) are not covered by this policy. ## Which Versions of this Software are Supported? The maintainer(s) will only commit to releasing security fixes for the latest version of Firefox-Marionette. Note that the Firefox-Marionette project only supports major versions of Perl released in the past ten (10) years, even though Firefox-Marionette will run on older versions of Perl. If a security fix requires us to increase the minimum version of Perl that is supported, then we may do that. # Installation and Usage Issues Please see the module documentation for more information. Firefox-Marionette-1.63/build-bcd-for-firefox0000755000175000017500000003643114763400566017613 0ustar davedave#! /usr/bin/perl use strict; use warnings; use Getopt::Long qw(:config bundling); use Cwd(); use File::Spec(); use File::Find(); use JSON(); use English qw( -no_match_vars ); use Fcntl(); use FileHandle(); use File::HomeDir(); use File::Temp(); use JSON(); use Carp(); use Firefox::Marionette(); local $ENV{PATH} = '/usr/bin:/bin:/usr/sbin:/sbin'; delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; sub _BUFFER_SIZE { return 8192 } sub _MAX_KEYS { return 3 } our $VERSION = '1.63'; MAIN: { my %options; Getopt::Long::GetOptions( \%options, 'help|h', 'version|v', 'path|p', ); my $bcd_path = Firefox::Marionette::BCD_PATH(1); _parse_options( $bcd_path, %options ); my ( $volume, $directories, $file ) = File::Spec->splitpath($bcd_path); my $firefox_marionette_directory = File::Spec->catdir( $volume, $directories ); my $browser_compat_data_directory = File::Spec->catdir( $firefox_marionette_directory, 'browser-compat-data' ); _setup_git_repos($browser_compat_data_directory); my $summary = {}; my $api_directory = File::Spec->catdir( $browser_compat_data_directory, 'api' ); my $builtins_directory = File::Spec->catdir( $browser_compat_data_directory, 'javascript', 'builtins' ); my $debug = 0; my $firefox = Firefox::Marionette->new( debug => $debug )->content() ->go('https://duckduckgo.com'); File::Find::find( { wanted => sub { if ( $File::Find::name =~ /[.]json$/smx ) { if ($debug) { Carp::carp("Looking at $File::Find::name\n") } my $path = $File::Find::name; my $handle = FileHandle->new( $path, Fcntl::O_RDONLY() ) or Carp::croak( "Failed to open $path for reading:$EXTENDED_OS_ERROR"); my $content; my $result; while ( $result = sysread $handle, my $buffer, _BUFFER_SIZE() ) { $content .= $buffer; } defined $result or Carp::croak( "Failed to read from $path:$EXTENDED_OS_ERROR"); close $handle or Carp::croak("Failed to close $path:$EXTENDED_OS_ERROR"); my $json = JSON::decode_json($content); my $root_element = _get_root_element( $json, $path ); foreach my $class_name ( sort { $a cmp $b } keys %{$root_element} ) { foreach my $browser (qw(firefox chrome edge safari ie opera)) { my $class_reference = $root_element->{$class_name}->{__compat} ->{support}->{$browser}; my $mirror_reference = $root_element->{$class_name}->{__compat} ->{support}->{chrome}; my @versions = _get_versions( $class_reference, $mirror_reference, $class_name ); foreach my $version ( sort { $a->{version_added} <=> $b->{version_added} } @versions ) { next if ( $version->{partial_implementation} ); _process_version( $summary, $class_name, $browser, $version, $path ); } foreach my $function_name ( sort { $a cmp $b } keys %{ $root_element->{$class_name} } ) { next if ( $function_name eq '__compat' ); next if ( $function_name eq 'worker_support' ); _process_function( $summary, root_element => $root_element, class_name => $class_name, function_name => $function_name, browser => $browser, path => $path ); } } } } }, follow => 1 }, $api_directory, $builtins_directory ); my ( $bcd_handle, $tmp_path ) = File::Temp::tempfile( 'firefox-marionette-bcd-XXXXXXXXXXX', DIR => $firefox_marionette_directory ); $bcd_handle->print( JSON->new()->pretty()->encode($summary) ) or Carp::croak("Failed to write to $tmp_path:$EXTENDED_OS_ERROR"); $bcd_handle->close() or Carp::croak("Failed to close $tmp_path:$EXTENDED_OS_ERROR"); rename $tmp_path, $bcd_path or Carp::croak("Failed to rename $tmp_path to $bcd_path:$EXTENDED_OS_ERROR"); } sub _parse_options { my ( $bcd_path, %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; } elsif ( $options{path} ) { print "$bcd_path\n" or die "Failed to print to STDOUT:$EXTENDED_OS_ERROR\n"; exit 0; } return; } sub _setup_git_repos { my ($browser_compat_data_directory) = @_; if ( -d $browser_compat_data_directory ) { my $cwd = Cwd::cwd(); chdir $browser_compat_data_directory or Carp::croak( "Failed to chdir $browser_compat_data_directory:$EXTENDED_OS_ERROR" ); system {'git'} 'git', 'pull', '--quiet' and Carp::croak( "Failed to git pull from $browser_compat_data_directory\n"); chdir $cwd or Carp::croak("Failed to chdir $cwd:$EXTENDED_OS_ERROR"); } else { my $mdn_browser_repo = 'https://github.com/mdn/browser-compat-data.git'; system {'git'} 'git', 'clone', $mdn_browser_repo, $browser_compat_data_directory and Carp::croak( "Failed to git clone $mdn_browser_repo $browser_compat_data_directory\n" ); } return; } sub _process_version { my ( $summary, $class_name, $browser, $version, $path ) = @_; if ( $version->{version_added} ) { if ( $version->{version_added} ne 'preview' ) { $summary->{$class_name}->{type} = 'class'; my %extra = _get_extra_from_flags( $version, $path ); push @{ $summary->{$class_name}->{browsers}->{$browser} }, { add => $version->{version_added}, %extra }; } } if ( $version->{version_removed} ) { push @{ $summary->{$class_name}->{browsers}->{$browser} }, { rm => $version->{version_removed} + 0 }; } return; } sub _process_function { my ( $summary, %parameters ) = @_; my $root_element = $parameters{root_element}; my $function_name = $parameters{function_name}; my $class_name = $parameters{class_name}; my $browser = $parameters{browser}; my $path = $parameters{path}; my $cleaned_function_name = $function_name; my $static; if ( $cleaned_function_name =~ s/_static$//smx ) { $static = 1; } my $function_reference = $root_element->{$class_name}->{$function_name}->{__compat}->{support} ->{$browser}; my $mirror_reference = $root_element->{$class_name}->{$function_name}->{__compat}->{support} ->{chrome}; my @versions = _get_versions( $function_reference, $mirror_reference, $function_name ); foreach my $version ( sort { $a->{version_added} <=> $b->{version_added} } @versions ) { # Example of partial_implementation to allow property to exist HTMLAnchorElement.ping # next if ( $version->{partial_implementation} ); # Example of note to allow property to exist - AudioBufferSourceNode.buffer # next if ( $version->{notes} ); if ( ( $version->{version_added} ) && ( $version->{version_added} ne 'preview' ) ) { $summary->{"$class_name.$cleaned_function_name"}->{type} = 'function'; $summary->{"$class_name.$cleaned_function_name"}->{static} = $static ? \1 : \0; my %extra = _get_extra_from_flags( $version, $path ); push @{ $summary->{"$class_name.$cleaned_function_name"}->{browsers} ->{$browser} }, { add => $version->{version_added}, %extra }; } if ( ( $version->{version_removed} ) && ( $version->{version_removed} ne 'preview' ) ) { push @{ $summary->{"$class_name.$cleaned_function_name"}->{browsers} ->{$browser} }, { rm => $version->{version_removed} + 0 }; } } return; } sub _get_extra_from_flags { my ( $version, $path ) = @_; my %extra; if ( $version->{flags} ) { foreach my $flag ( @{ $version->{flags} } ) { if ( ( defined $flag->{type} ) && ( defined $flag->{name} ) && ( defined $flag->{value_to_set} ) && ( ( keys %{$flag} ) == _MAX_KEYS() ) ) { } elsif (( defined $flag->{type} ) && ( defined $flag->{name} ) && ( !defined $flag->{value_to_set} ) && ( ( keys %{$flag} ) == 2 ) ) { next; } else { Carp::croak("Unknown flag for $path"); } if ( $flag->{type} eq 'preference' ) { %extra = ( pref_name => $flag->{name}, pref_value => $flag->{value_to_set} ); } else { Carp::croak("Unknown type of '$flag->{type}' in $path"); } } } return %extra; } sub _get_root_element { my ( $json, $path ) = @_; my $root_element; my @parts; while ( !$root_element ) { my $missed; foreach my $key ( keys %{$json} ) { if ( exists $json->{$key}->{__compat} ) { my $full_key = join q[.], @parts, $key; $full_key =~ s/^javascript[.]builtins[.]//smx; $full_key =~ s/^api[.]//smx; $root_element->{$full_key} = $json->{$key}; } else { $json = $json->{$key}; push @parts, $key; last; } } if ($root_element) { if ($missed) { Carp::croak("Failed to navigate JSON in $path for key $missed"); } } } return $root_element; } sub _get_versions { my ( $object, $mirror, $name ) = @_; my @versions; if ($object) { if ( $object eq 'mirror' ) { $object = $mirror; } if ( ( ref $object ) eq 'HASH' ) { push @versions, _strip_version($object); } else { push @versions, _strip_version( @{$object} ); } } return @versions; } sub _strip_version { my (@possible) = @_; my @approved; foreach my $version (@possible) { if ( $version->{version_added} ) { if ( $version->{version_added} eq 'preview' ) { next; } elsif ( $version->{version_added} =~ s/^\x{2264}(\d+(?:[.]\d+)?)$/$1/smx ) { } } push @approved, $version; } return @approved; } __END__ =head1 NAME build-bcd-for-firefox - build user agent data from the @mdn/browser-compat-data repo =head1 VERSION Version 1.63 =head1 USAGE $ build-bcd-for-firefox $ build-bcd-for-firefox --path =head1 DESCRIPTION This program is intended to build a database for the agent method of the Firefox::Marionette class. It builds this database by cloning the L<@mdn/browser-compat-data|https://github.com/mdn/browser-compat-data> repository on github.com and then summarising this data for the agent method. The path where the database is stored varies by user and operating system, and can be shown with the --path option. =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 current version of this binary =item * --path - Print the path of the local database that is getting built. =back =head1 CONFIGURATION build-bcd-for-firefox requires no configuration files or environment variables. =head1 DEPENDENCIES build-bcd-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) 2024, 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.63/META.yml0000644000175000017500000000307414763402246015057 0ustar davedave--- abstract: 'Automate the Firefox browser with the Marionette protocol' author: - 'David Dick ' build_requires: Compress::Zlib: '0' Crypt::PasswdMD5: '0' Cwd: '0' Digest::SHA: '0' File::HomeDir: '0' HTTP::Daemon: '0' HTTP::Response: '0' HTTP::Status: '0' IO::Socket::IP: '0' IO::Socket::SSL: '0' PDF::API2: '2.036' Test::CheckManifest: '0.9' Test::More: '0' Test::Pod: '1.41' Test::Pod::Coverage: '1.04' configure_requires: ExtUtils::MakeMaker: '0' dynamic_config: 1 generated_by: 'ExtUtils::MakeMaker version 7.70, 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' IO::Handle: '0' IPC::Open3: '1.03' JSON: '0' MIME::Base64: '3.11' POSIX: '0' Pod::Simple::Text: '0' Scalar::Util: '0' Socket: '0' Term::ReadKey: '0' Text::CSV_XS: '1.35' Time::HiRes: '0' Time::Local: '0' URI: '1.61' URI::Escape: '0' URI::URL: '0' URI::data: '0' XML::Parser: '0' overload: '0' parent: '0' perl: '5.010' resources: bugtracker: https://github.com/david-dick/firefox-marionette/issues repository: https://github.com/david-dick/firefox-marionette version: '1.63' x_serialization_backend: 'CPAN::Meta::YAML version 0.018'