Firefox-Marionette/lib/Firefox/Marionette.pm
package 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.68';
sub _ANYPROCESS { return -1 }
sub _PROCESS_GROUP { 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{system_access} ) {
$self->{system_access} = $parameters{system_access};
}
else {
$self->{system_access} = 1;
}
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 serviceResult;
if (lazy.PlacesUtils.favicons.getFaviconForPage) {
serviceResult = await PlacesUtils.favicons.getFaviconForPage(url, 0);
}
let addFavicon = function(pageUrl) {
return new Promise((resolve, reject) => {
if (PlacesUtils.favicons.getFaviconForPage) {
/* https://bugzilla.mozilla.org/show_bug.cgi?id=1915762 */
if (serviceResult === null ) {
resolve([]);
} else {
resolve([ serviceResult.uri, serviceResult.rawData.length, serviceResult.rawData, serviceResult.mimeType, serviceResult.width ]);
}
} else {
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_';
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file.
It will be read and overwritten.
DO NOT EDIT! -->
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
_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[^>]+><\/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() eq $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->{debug} ) && ( $self->{debug} !~ /^[01]$/smx ) ) {
push @arguments, '-MOZ_LOG=' . $self->{debug};
}
if ( $self->{system_access} ) {
push @arguments, '-remote-allow-system-access';
}
if ( defined $self->{window_width} ) {
push @arguments, '-width', $self->{window_width};
}
if ( defined $self->{window_height} ) {
push @arguments, '-height', $self->{window_height};
}
if ( defined $self->{console} ) {
push @arguments, '--jsconsole';
}
push @arguments, $self->_check_addons(%parameters);
push @arguments, $self->_check_visible(%parameters);
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_';
<?xml version="1.0"?>
<RDF:RDF xmlns:NC="http://home.netscape.com/NC-rdf#"
xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<RDF:Seq RDF:about="urn:mimetypes:root">
_RDF_
foreach my $mime_type ( @{ $self->{mime_types} } ) {
$mime_types_content .= <<'_RDF_';
<RDF:li RDF:resource="urn:mimetype:$mime_type"/>
_RDF_
}
$mime_types_content .= <<'_RDF_';
</RDF:Seq>
<RDF:Description RDF:about="urn:root"
NC:en-US_defaultHandlersVersion="4" />
<RDF:Description RDF:about="urn:mimetypes">
<NC:MIME-types RDF:resource="urn:mimetypes:root"/>
</RDF:Description>
_RDF_
foreach my $mime_type ( @{ $self->{mime_types} } ) {
$mime_types_content .= <<'_RDF_';
<RDF:Description RDF:about="urn:mimetype:handler:$mime_type"
NC:saveToDisk="true"
NC:alwaysAsk="false" />
<RDF:Description RDF:about="urn:mimetype:$mime_type"
NC:value="$mime_type">
<NC:handlerProp RDF:resource="urn:mimetype:handler:$mime_type"/>
</RDF:Description>
_RDF_
}
$mime_types_content .= <<'_RDF_';
</RDF: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 {
setpgrp 0, 0
or Firefox::Marionette::Exception->throw(
"Failed to set process group (setpgrp):$EXTENDED_OS_ERROR");
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_";
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file.
It will be read and overwritten.
DO NOT EDIT! -->
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks Menu</H1>
<DL><p>
<DT><H3 ADD_DATE="$now" LAST_MODIFIED="$now" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Toolbar</H3>
<DL><p>
</DL><p>
<DT><H3 ADD_DATE="$now" LAST_MODIFIED="$now" UNFILED_BOOKMARKS_FOLDER="true">Other Bookmarks</H3>
<DL><p>
</DL><p>
</DL>
_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 delete_element {
my ( $self, $element ) = @_;
if (
!$self->_is_marionette_object(
$element, 'Firefox::Marionette::Element'
)
)
{
Firefox::Marionette::Exception->throw(
'delete_element method requires a Firefox::Marionette::Element parameter');
}
$self->script('arguments[0].remove()', args => [ $element ]);
return $self;
}
sub click {
my ( $self, $element ) = @_;
if (
!$self->_is_marionette_object(
$element, 'Firefox::Marionette::Element'
)
)
{
Firefox::Marionette::Exception->throw(
'click method requires a Firefox::Marionette::Element parameter');
}
my $message_id = $self->_new_message_id();
$self->_send_request(
[
_COMMAND(), $message_id,
$self->_command('WebDriver:ElementClick'),
{ id => $element->uuid() }
]
);
my $response = $self->_get_response($message_id);
return $self;
}
sub timeouts {
my ( $self, $new ) = @_;
my $old;
if ( $self->{_no_timeouts_command} ) {
if ( !defined $self->{_no_timeouts_command}->{page_load} ) {
$self->{_no_timeouts_command} = $new;
}
$old = $self->{_no_timeouts_command};
}
else {
my $message_id = $self->_new_message_id();
$self->_send_request(
[
_COMMAND(), $message_id,
$self->_command('WebDriver:GetTimeouts')
]
);
my $response = $self->_get_response($message_id);
$old = Firefox::Marionette::Timeouts->new(
page_load => $response->result()
->{ $self->{_cached_per_instance}->{_page_load_timeouts_key} },
script => $response->result()->{script},
implicit => $response->result()->{implicit}
);
}
if ( defined $new ) {
if ( $self->{_no_timeouts_command} ) {
my $message_id = $self->_new_message_id();
$self->_send_request(
[
_COMMAND(),
$message_id,
'timeouts',
{
type => 'implicit',
ms => $new->implicit(),
}
]
);
$self->_get_response($message_id);
$message_id = $self->_new_message_id();
$self->_send_request(
[
_COMMAND(),
$message_id,
'timeouts',
{
type => 'script',
ms => $new->script(),
}
]
);
$self->_get_response($message_id);
$message_id = $self->_new_message_id();
$self->_send_request(
[
_COMMAND(),
$message_id,
'timeouts',
{
type => 'default',
ms => $new->page_load(),
}
]
);
$self->_get_response($message_id);
$self->{_no_timeouts_command} = $new;
}
else {
my $message_id = $self->_new_message_id();
$self->_send_request(
[
_COMMAND(),
$message_id,
$self->_command('WebDriver:SetTimeouts'),
{
$self->{_cached_per_instance}
->{_page_load_timeouts_key} => $new->page_load(),
script => $new->script(),
implicit => $new->implicit()
}
]
);
$self->_get_response($message_id);
}
}
return $old;
}
sub active_element {
my ($self) = @_;
my $message_id = $self->_new_message_id();
$self->_send_request(
[
_COMMAND(), $message_id,
$self->_command('WebDriver:GetActiveElement')
]
);
my $response = $self->_get_response($message_id);
if ( ref $self->_response_result_value($response) ) {
return Firefox::Marionette::Element->new( $self,
%{ $self->_response_result_value($response) } );
}
else {
return Firefox::Marionette::Element->new( $self,
ELEMENT => $self->_response_result_value($response) );
}
}
sub uri {
my ($self) = @_;
my $message_id = $self->_new_message_id();
$self->_send_request(
[ _COMMAND(), $message_id, $self->_command('WebDriver:GetCurrentURL') ]
);
my $response = $self->_get_response($message_id);
return URI->new( $self->_response_result_value($response) );
}
sub full_screen {
my ($self) = @_;
my $message_id = $self->_new_message_id();
$self->_send_request(
[
_COMMAND(), $message_id,
$self->_command('WebDriver:FullscreenWindow')
]
);
my $response = $self->_get_response($message_id);
return $self;
}
sub dismiss_alert {
my ($self) = @_;
my $message_id = $self->_new_message_id();
$self->_send_request(
[ _COMMAND(), $message_id, $self->_command('WebDriver:DismissAlert') ]
);
my $response = $self->_get_response($message_id);
return $self;
}
sub send_alert_text {
my ( $self, $text ) = @_;
my $message_id = $self->_new_message_id();
my $parameters = { text => $text };
if ( !$self->_is_new_sendkeys_okay() ) {
$parameters->{value} = [ split //smx, $text ];
}
$self->_send_request(
[
_COMMAND(), $message_id,
$self->_command('WebDriver:SendAlertText'), $parameters
]
);
my $response = $self->_get_response($message_id);
return $self;
}
sub accept_alert {
my ($self) = @_;
my $message_id = $self->_new_message_id();
$self->_send_request(
[ _COMMAND(), $message_id, $self->_command('WebDriver:AcceptAlert') ] );
my $response = $self->_get_response($message_id);
return $self;
}
sub accept_dialog {
my ($self) = @_;
Carp::carp(
'**** DEPRECATED METHOD - using accept_dialog() HAS BEEN REPLACED BY accept_alert ****'
);
my $message_id = $self->_new_message_id();
$self->_send_request(
[ _COMMAND(), $message_id, $self->_command('WebDriver:AcceptDialog') ]
);
my $response = $self->_get_response($message_id);
return $self;
}
sub alert_text {
my ($self) = @_;
my $message_id = $self->_new_message_id();
$self->_send_request(
[ _COMMAND(), $message_id, $self->_command('WebDriver:GetAlertText') ]
);
my $response = $self->_get_response($message_id);
return $self->_response_result_value($response);
}
my %_pdf_sizes = (
# '4A0' => { width => 168.2, height => 237.8 },
# '2A0' => { width => 118.9, height => 168.2 },
# A9 => { width => 3.7, height => 5.2 },
# A10 => { width => 2.6, height => 3.7 },
# B0 => { width => 100, height => 141.4 },
A1 => { width => 59.4, height => 84.1 },
A2 => { width => 42, height => 59.4 },
A3 => { width => 29.7, height => 42 },
A4 => { width => 21, height => 29.7 },
A5 => { width => 14.8, height => 21 },
A6 => { width => 10.5, height => 14.8 },
A7 => { width => 7.4, height => 10.5 },
A8 => { width => 5.2, height => 7.4 },
B1 => { width => 70.7, height => 100 },
B2 => { width => 50, height => 70.7 },
B3 => { width => 35.3, height => 50 },
B4 => { width => 25, height => 35.3 },
B5 => { width => 17.6, height => 25 },
B6 => { width => 12.5, height => 17.6 },
B7 => { width => 8.8, height => 12.5 },
B8 => { width => 6.2, height => 8.8 },
HALF_LETTER => { width => 14, height => 21.6 },
LETTER => { width => 21.6, height => 27.9 },
LEGAL => { width => 21.6, height => 35.6 },
JUNIOR_LEGAL => { width => 12.7, height => 20.3 },
LEDGER => { width => 12.7, height => 20.3 },
);
sub paper_sizes {
my @keys = sort { $a cmp $b } keys %_pdf_sizes;
return @keys;
}
sub _map_deprecated_pdf_parameters {
my ( $self, %parameters ) = @_;
my %mapping = (
shrink_to_fit => 'shrinkToFit',
print_background => 'printBackground',
page_ranges => 'pageRanges',
);
foreach my $from ( sort { $a cmp $b } keys %mapping ) {
my $to = $mapping{$from};
if ( defined $parameters{$to} ) {
Carp::carp(
"**** DEPRECATED PARAMETER - using $to as a parameter for the pdf(...) method HAS BEEN REPLACED BY the $from parameter ****"
);
}
elsif ( defined $parameters{$from} ) {
$parameters{$to} = $parameters{$from};
delete $parameters{$from};
}
}
foreach my $key ( sort { $a cmp $b } keys %parameters ) {
next if ( $key eq 'landscape' );
next if ( $key eq 'shrinkToFit' );
next if ( $key eq 'printBackground' );
next if ( $key eq 'margin' );
next if ( $key eq 'page' );
next if ( $key eq 'pageRanges' );
next if ( $key eq 'size' );
next if ( $key eq 'raw' );
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() * _PROCESS_GROUP() ) )
{
$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() * _PROCESS_GROUP() ) {
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() * _PROCESS_GROUP() ) )
{
$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}
]
);
my $response = $self->_get_response($message_id);
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><link[^>]+><\/head>/smx;
my $script_regex = qr/(?:<script[^>]+><\/script>)?/smx;
my $header = qr/<html[^>]*>$script_regex$head_regex<body><pre>/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.68
=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<Marionette protocol|https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/Protocol>
=head1 CONSTANTS
=head2 BCD_PATH
returns the local path used for storing the brower compability data for the L<agent|/agent> method when the C<stealth> parameter is supplied to the L<new|/new> method. This database is built by the L<build-bcd-for-firefox|https://metacpan.org/pod/build-bcd-for-firefox> 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<bookmark|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 L<itself|Firefox::Marionette> 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<PEM encoded X.509 certificate|https://datatracker.ietf.org/doc/html/rfc7468#section-5>.
=item * string - a string containing a single L<PEM encoded X.509 certificate|https://datatracker.ietf.org/doc/html/rfc7468#section-5>
=item * trust - This is the L<trustargs|https://www.mankier.com/1/certutil#-t> value for L<NSS|https://wiki.mozilla.org/NSS>. If defaults to 'C,,';
=back
This method returns L<itself|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);
=head2 add_cookie
accepts a single L<cookie|Firefox::Marionette::Cookie> object as the first parameter and adds it to the current cookie jar. This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
This method throws an exception if you try to L<add a cookie for a different domain than the current document|https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors/InvalidCookieDomain>.
=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<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 L<Accept|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept> header that looks like C<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 L<Accept|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept> header that may resemble C<Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8, text/perl>. This method returns L<itself|Firefox::Marionette> 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<was submitted to|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-action>. Forms with no L<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.
=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<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.
=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<Firefox::Marionette::Login|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 L<realm|https://datatracker.ietf.org/doc/html/rfc2617#section-2> must perfectly match the correct L<realm|https://datatracker.ietf.org/doc/html/rfc2617#section-2> supplied by the server.
This method returns L<itself|Firefox::Marionette> 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<delete_site_header|/delete_site_header> 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<client side discoverable credentials|https://www.w3.org/TR/webauthn-2/#client-side-discoverable-credential>
=item * has_user_verification - boolean value to determine if the L<authenticator|https://www.w3.org/TR/webauthn-2/#virtual-authenticators> supports L<user verification|https://www.w3.org/TR/webauthn-2/#user-verification>.
=item * is_user_consenting - boolean value to determine the result of all L<user consent|https://www.w3.org/TR/webauthn-2/#user-consent> L<authorization gestures|https://www.w3.org/TR/webauthn-2/#authorization-gesture>, and by extension, any L<test of user presence|https://www.w3.org/TR/webauthn-2/#test-of-user-presence> performed on the L<Virtual Authenticator|https://www.w3.org/TR/webauthn-2/#virtual-authenticators>. If set to true, a L<user consent|https://www.w3.org/TR/webauthn-2/#user-consent> 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<User Verification|https://www.w3.org/TR/webauthn-2/#user-verification> performed on the L<Virtual Authenticator|https://www.w3.org/TR/webauthn-2/#virtual-authenticators>. If set to true, L<User Verification|https://www.w3.org/TR/webauthn-2/#user-verification> will always succeed. If set to false, it will fail.
=item * protocol - the L<protocol|Firefox::Marionette::WebAuthn::Authenticator#protocol> spoken by the authenticator. This may be L<CTAP1_U2F|Firefox::Marionette::WebAuthn::Authenticator#CTAP1_U2F>, L<CTAP2|Firefox::Marionette::WebAuthn::Authenticator#CTAP2> or L<CTAP2_1|Firefox::Marionette::WebAuthn::Authenticator#CTAP2_1>.
=item * transport - the L<transport|Firefox::Marionette::WebAuthn::Authenticator#transport> simulated by the authenticator. This may be L<BLE|Firefox::Marionette::WebAuthn::Authenticator#BLE>, L<HYBRID|Firefox::Marionette::WebAuthn::Authenticator#HYBRID>, L<INTERNAL|Firefox::Marionette::WebAuthn::Authenticator#INTERNAL>, L<NFC|Firefox::Marionette::WebAuthn::Authenticator#NFC>, L<SMART_CARD|Firefox::Marionette::WebAuthn::Authenticator#SMART_CARD> or L<USB|Firefox::Marionette::WebAuthn::Authenticator#USB>.
=back
It returns the newly created L<authenticator|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'); });
=head2 add_webauthn_credential
accepts a hash of the following keys;
=over 4
=item * authenticator - contains the L<authenticator|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.
=item * host - contains the domain that this credential is to be used for. In the language of L<WebAuthn|https://www.w3.org/TR/webauthn-2>, this field is referred to as the L<relying party identifier|https://www.w3.org/TR/webauthn-2/#relying-party-identifier> or L<RP ID|https://www.w3.org/TR/webauthn-2/#rp-id>.
=item * id - contains the unique id for this credential, also known as the L<Credential ID|https://www.w3.org/TR/webauthn-2/#credential-id>. If this is not supplied, one will be generated.
=item * is_resident - contains a boolean that if set to true, a L<client-side discoverable credential|https://w3c.github.io/webauthn/#client-side-discoverable-credential> is created. If set to false, a L<server-side credential|https://w3c.github.io/webauthn/#server-side-credential> is created instead.
=item * private_key - either a L<RFC5958|https://www.rfc-editor.org/rfc/rfc5958> encoded private key encoded using L<encode_base64url|MIME::Base64::encode_base64url> 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<signature counter|https://w3c.github.io/webauthn/#signature-counter> associated to the L<public key credential source|https://w3c.github.io/webauthn/#public-key-credential-source>. It will default to 0 (zero).
=item * user - contains the L<userHandle|https://w3c.github.io/webauthn/#public-key-credential-source-userhandle> associated to the credential encoded using L<encode_base64url|MIME::Base64::encode_base64url>. This property is optional.
=back
It returns the newly created L<credential|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'); });
=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<User-Agent|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent> header and sets this using the profile preferences and inserting L<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 L<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 L<Chrome|https://www.google.com/chrome/>, L<Edge|https://microsoft.com/edge> or L<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;
=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<stealth> parameter has supplied to the L<new|/new> method, it will also attempt to create known specific javascript functions to imitate the required browser. If the database built by L<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 L<javascript attributes|https://github.com/mdn/browser-compat-data> for the desired browser. The following websites have been very useful in testing these ideas;
=over 4
=item * L<https://browserleaks.com/javascript>
=item * L<https://www.amiunique.org/fingerprint>
=item * L<https://bot.sannysoft.com/>
=item * L<https://lraj22.github.io/browserfeatcl/>
=back
Importantly, this will break L<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 L<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.
=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<element|Firefox::Marionette::Element> as the parameter. It returns the L<ARIA label|https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label> for the L<element|Firefox::Marionette::Element>.
=head2 aria_role
accepts an L<element|Firefox::Marionette::Element> as the parameter. It returns the L<ARIA role|https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles> for the L<element|Firefox::Marionette::Element>.
=head2 async_script
accepts a scalar containing a javascript function that is executed in the browser. This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
The executing javascript is subject to the L<script|Firefox::Marionette::Timeouts#script> timeout, which, by default is 30 seconds.
=head2 attribute
accepts an L<element|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 L<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!";
=head2 await
accepts a subroutine reference as a parameter and then executes the subroutine. If a L<not found|Firefox::Marionette::Exception::NotFound> exception is thrown, this method will sleep for L<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();
=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<page_load|Firefox::Marionette::Timeouts#page_load> duration to elapse before returning, which, by default is 5 minutes. This method returns L<itself|Firefox::Marionette> 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<URI|URI> and an optional timeout in seconds (the default is 5 minutes) as parameters and downloads the L<URI|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;
}
=head2 bookmarks
accepts either a scalar or a hash as a parameter. The scalar may by the title of a bookmark or the L<URL|URI::URL> 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<Firefox::Marionette::Bookmark|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";
}
=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<sleep_time_in_ms|/sleep_time_in_ms> milliseconds and then execute the subroutine again. When a L<not found|Firefox::Marionette::Exception::NotFound> exception is thrown, this method will return L<itself|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();
=head2 cache_keys
returns the set of all cache keys from L<Firefox::Marionette::Cache|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";
}
}
=head2 capabilities
returns the L<capabilities|Firefox::Marionette::Capabilities> of the current firefox binary. You can retrieve L<timeouts|Firefox::Marionette::Timeouts> or a L<proxy|Firefox::Marionette::Proxy> with this method.
=head2 certificate_as_pem
accepts a L<certificate stored in the Firefox database|Firefox::Marionette::Certificate> as a parameter and returns a L<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 L<ca-bundle-for-firefox|https://metacpan.org/pod/ca-bundle-for-firefox> command that is provided as part of this distribution does this.
=head2 certificates
returns a list of all known L<certificates in the Firefox database|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 L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 check_cache_key
accepts a L<cache_key|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 L<cache_key|Firefox::Marionette::Cache>'s actual value from firefox as a number. This may differ from the current value of the key from L<Firefox::Marionette::Cache|Firefox::Marionette::Cache> 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<context|/context> method for an alternative methods for changing the context.
=head2 chrome_window_handle
returns a L<server-assigned identifier for the current chrome window that uniquely identifies it|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 L<window_handle|/window_handle> and appropriate L<context|/context> calls for L<Firefox 94 and after|https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/94#webdriver_conformance_marionette>.
=head2 chrome_window_handles
returns L<identifiers|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 L<window_handles|/window_handles> and appropriate L<context|/context> calls for L<Firefox 94 and after|https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/94#webdriver_conformance_marionette>.
=head2 clear
accepts a L<element|Firefox::Marionette::Element> 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<Firefox::Marionette::Cache|Firefox::Marionette::Cache> and clears the appropriate sections of the cache. If no flags parameter is supplied, the default is L<CLEAR_ALL|Firefox::Marionette::Cache#CLEAR_ALL>. Note that this method, unlike L<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 L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 clear_pref
accepts a L<preference|http://kb.mozillazine.org/About:config> name and restores it to the original value. See the L<get_pref|/get_pref> and L<set_pref|/set_pref> methods to get a preference value and to set to it to a particular value. This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
use Firefox::Marionette();
my $firefox = Firefox::Marionette->new();
$firefox->clear_pref('browser.search.defaultenginename');
=head2 click
accepts a L<element|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 L<page_load|Firefox::Marionette::Timeouts#page_load> duration to elapse before returning, which, by default is 5 minutes. The L<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();
}
}
=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<chrome window handles|Firefox::Marionette::WebWindow>. You will need to L<switch_to_window|/switch_to_window> to use another window.
=head2 close_current_window_handle
closes the current window/tab. It returns a list of still available L<windowE<sol>tab 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<context|/context> 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<content|/content> and L<chrome|/chrome> methods for alternative methods for changing the context.
=head2 cookies
returns the L<contents|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";
}
}
=head2 css
accepts an L<element|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');
=head2 current_chrome_window_handle
see L<chrome_window_handle|/chrome_window_handle>.
=head2 delete_bookmark
accepts a L<bookmark|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 L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 delete_certificate
accepts a L<certificate stored in the Firefox database|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 L<itself|Firefox::Marionette> 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<itself|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";
}
=head2 delete_cookies
Here be cookie monsters! Note that this method will only delete cookies for the current site. See L<clear_cache|/clear_cache> for an alternative. This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 delete_element
accepts a L<element|Firefox::Marionette::Element> as the first parameter and L<delete|https://developer.mozilla.org/en-US/docs/Web/API/Element/remove>'s it from the DOM.
use Firefox::Marionette();
my $firefox = Firefox::Marionette->new(visible => 1)->go('https://ebay.com');
my $select = $firefox->find_tag('select');
$firefox->delete_element($select);
This method returns L<itself|Firefox::Marionette> 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<User-Agent|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent>, L<Accept|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept> and L<Accept-Encoding|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding> headers from all future requests
This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 delete_login
accepts a L<login|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 L<itself|Firefox::Marionette> 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<itself|Firefox::Marionette> 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<User-Agent|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent>, L<Accept|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept> and L<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 L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 delete_webauthn_all_credentials
This method accepts an optional L<authenticator|Firefox::Marionette::WebAuthn::Authenticator>, in which case it will delete all L<credentials|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();
=head2 delete_webauthn_authenticator
This method accepts an optional L<authenticator|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();
=head2 delete_webauthn_credential
This method accepts either a L<credential|Firefox::Marionette::WebAuthn::Credential> and an L<authenticator|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 L<credential|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 L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 developer
returns true if the L<current version|/browser_version> of firefox is a L<developer edition|https://www.mozilla.org/en-US/firefox/developer/> (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<usage for the display|Firefox::Marionette::Display#usage> and returns a list of all the L<known displays|https://en.wikipedia.org/wiki/List_of_common_resolutions> as a L<Firefox::Marionette::Display|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!';
}
}
=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<downloads|/downloads> 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<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();
=head2 find
accepts an L<xpath expression|https://en.wikipedia.org/wiki/XPath> as the first parameter and returns the first L<element|Firefox::Marionette::Element> that matches this expression.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception will be thrown. For the same functionality that returns undef if no elements are found, see the L<has|/has> method.
=head2 find_id
accepts an L<id|https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id> as the first parameter and returns the first L<element|Firefox::Marionette::Element> with a matching 'id' property.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception will be thrown. For the same functionality that returns undef if no elements are found, see the L<has_id|/has_id> method.
=head2 find_name
This method returns the first L<element|Firefox::Marionette::Element> with a matching 'name' property.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception will be thrown. For the same functionality that returns undef if no elements are found, see the L<has_name|/has_name> method.
=head2 find_class
accepts a L<class name|https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class> as the first parameter and returns the first L<element|Firefox::Marionette::Element> with a matching 'class' property.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception will be thrown. For the same functionality that returns undef if no elements are found, see the L<has_class|/has_class> method.
=head2 find_selector
accepts a L<CSS Selector|https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors> as the first parameter and returns the first L<element|Firefox::Marionette::Element> that matches that selector.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception will be thrown. For the same functionality that returns undef if no elements are found, see the L<has_selector|/has_selector> method.
=head2 find_tag
accepts a L<tag name|https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName> as the first parameter and returns the first L<element|Firefox::Marionette::Element> with this tag name.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception will be thrown. For the same functionality that returns undef if no elements are found, see the L<has_tag|/has_tag> method.
=head2 find_link
accepts a text string as the first parameter and returns the first link L<element|Firefox::Marionette::Element> that has a matching link text.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception will be thrown. For the same functionality that returns undef if no elements are found, see the L<has_link|/has_link> method.
=head2 find_partial
accepts a text string as the first parameter and returns the first link L<element|Firefox::Marionette::Element> that has a partially matching link text.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception will be thrown. For the same functionality that returns undef if no elements are found, see the L<has_partial|/has_partial> 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<page_load|Firefox::Marionette::Timeouts#page_load> duration to elapse before returning, which, by default is 5 minutes. This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 full_screen
full screens the firefox window. This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 geo
accepts an optional L<geo location|Firefox::Marionette::GeoLocation> object or the parameters for a L<geo location|Firefox::Marionette::GeoLocation> object, turns on the L<Geolocation API|https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API> and returns the current L<value|Firefox::Marionette::GeoLocation> returned by calling the javascript L<getCurrentPosition|https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition> method. This method is further discussed in the L<GEO LOCATION|/GEO-LOCATION> section. If the current location cannot be determined, this method will return undef.
NOTE: firefox will only allow L<Geolocation|https://developer.mozilla.org/en-US/docs/Web/API/Geolocation> calls to be made from L<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 L<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 L<geo location|Firefox::Marionette::GeoLocation> parameter) to match the specified L<timezone|Firefox::Marionette::GeoLocation#tz>. This function should be considered experimental. Feedback welcome.
If particular, the L<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.
=head2 go
Navigates the current browsing context to the given L<URI|URI> and waits for the document to load or the session's L<page_load|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 L<go|/go> method return quicker, you need to set the L<page load strategy|Firefox::Marionette::Capabilities#page_load_strategy> L<capability|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 L<BUGS AND LIMITATIONS|/DOWNLOADING-USING-GO-METHOD> for a necessary workaround and the L<download|/download> method for an alternative.
This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 get_pref
accepts a L<preference|http://kb.mozillazine.org/About:config> name. See the L<set_pref|/set_pref> and L<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');
=head2 har
returns a hashref representing the L<http archive|https://en.wikipedia.org/wiki/HAR_(file_format)> of the session. This function is subject to the L<script|Firefox::Marionette::Timeouts#script> timeout, which, by default is 30 seconds. It is also possible for the function to hang (until the L<script|Firefox::Marionette::Timeouts#script> timeout) if the original L<devtools|https://developer.mozilla.org/en-US/docs/Tools> window is closed. The hashref has been designed to be accepted by the L<Archive::Har|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";
}
=head2 has
accepts an L<xpath expression|https://en.wikipedia.org/wiki/XPath> as the first parameter and returns the first L<element|Firefox::Marionette::Element> that matches this expression.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception, see the L<find|/find> method.
=head2 has_id
accepts an L<id|https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id> as the first parameter and returns the first L<element|Firefox::Marionette::Element> with a matching 'id' property.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception, see the L<find_id|/find_id> method.
=head2 has_name
This method returns the first L<element|Firefox::Marionette::Element> with a matching 'name' property.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception, see the L<find_name|/find_name> method.
=head2 has_class
accepts a L<class name|https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class> as the first parameter and returns the first L<element|Firefox::Marionette::Element> with a matching 'class' property.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception, see the L<find_class|/find_class> method.
=head2 has_selector
accepts a L<CSS Selector|https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors> as the first parameter and returns the first L<element|Firefox::Marionette::Element> that matches that selector.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception, see the L<find_selector|/find_selector> method.
=head2 has_tag
accepts a L<tag name|https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName> as the first parameter and returns the first L<element|Firefox::Marionette::Element> with this tag name.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception, see the L<find_tag|/find_tag> method.
=head2 has_link
accepts a text string as the first parameter and returns the first link L<element|Firefox::Marionette::Element> that has a matching link text.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception, see the L<find_link|/find_link> method.
=head2 has_partial
accepts a text string as the first parameter and returns the first link L<element|Firefox::Marionette::Element> that has a partially matching link text.
This method is subject to the L<implicit|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 L<not found|Firefox::Marionette::Exception::NotFound> exception, see the L<find_partial|/find_partial> 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<json|/json> method for an alternative when dealing with response content types such as application/json and L<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();
=head2 import_bookmarks
accepts a filesystem path to a bookmarks file and imports all the L<bookmarks|Firefox::Marionette::Bookmark> in that file. It can deal with backups from L<Firefox|https://support.mozilla.org/en-US/kb/export-firefox-bookmarks-to-backup-or-transfer>, L<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 L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 images
returns a list of all of the following elements;
=over 4
=item * L<img|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img>
=item * L<image inputs|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/image>
=back
as L<Firefox::Marionette::Image|Firefox::Marionette::Image> objects.
This method is subject to the L<implicit|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.
=head2 install
accepts the following as the first parameter;
=over 4
=item * path to an L<xpi file|https://developer.mozilla.org/en-US/docs/Mozilla/XPI>.
=item * path to a directory containing L<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.
=item * path to a top level file (such as L<manifest.json|https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Anatomy_of_a_WebExtension#manifest.json>) in a directory containing L<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.
=back
and an optional true/false second parameter to indicate if the xpi file should be a L<temporary extension|https://extensionworkshop.com/documentation/develop/temporary-installation-in-firefox/> (just for the existence of this browser instance). Unsigned xpi files L<may only be loaded temporarily|https://wiki.mozilla.org/Add-ons/Extension_Signing> (except for L<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 L<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
}
=head2 interactive
returns true if C<document.readyState === "interactive"> or if L<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
}
=head2 is_displayed
accepts an L<element|Firefox::Marionette::Element> as the first parameter. This method returns true or false depending on if the element L<is displayed|https://firefox-source-docs.mozilla.org/testing/marionette/internals/interaction.html#interaction.isElementDisplayed>.
=head2 is_enabled
accepts an L<element|Firefox::Marionette::Element> as the first parameter. This method returns true or false depending on if the element L<is enabled|https://w3c.github.io/webdriver/#is-element-enabled>.
=head2 is_selected
accepts an L<element|Firefox::Marionette::Element> as the first parameter. This method returns true or false depending on if the element L<is selected|https://w3c.github.io/webdriver/#dfn-is-element-selected>. Note that this method only makes sense for L<checkbox|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox> or L<radio|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio> inputs or L<option|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option> elements in a L<select|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select> dropdown.
=head2 is_trusted
accepts an L<certificate|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";
}
}
=head2 json
returns a L<JSON|JSON> object that has been parsed from the page source of the content document. This is a convenience method that wraps the L<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 L<URI|URI> as a parameter and retrieve that URI via the firefox L<fetch call|https://developer.mozilla.org/en-US/docs/Web/API/fetch> and transforming the body to L<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};
=head2 key_down
accepts a parameter describing a key and returns an action for use in the L<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();
=head2 key_up
accepts a parameter describing a key and returns an action for use in the L<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();
=head2 languages
accepts an optional list of values for the L<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').
=head2 loaded
returns true if C<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
}
=head2 logins
returns a list of all L<Firefox::Marionette::Login|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();
}
=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<Bitwarden CSV|https://bitwarden.com/help/article/condition-bitwarden-import/>
=item * L<LastPass CSV|https://support.logmeininc.com/lastpass/help/how-do-i-nbsp-export-stored-data-from-lastpass-using-a-generic-csv-file>
=item * L<KeePass CSV|https://keepass.info/help/base/importexport.html#csv>
=back
returns a list of L<Firefox::Marionette::Login|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);
}
=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<KeePass 1.x XML|https://keepass.info/help/base/importexport.html#xml>
=back
returns a list of L<Firefox::Marionette::Login|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);
}
=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<Firefox::Marionette::Login|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);
}
=head2 links
returns a list of all of the following elements;
=over 4
=item * L<anchor|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a>
=item * L<area|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area>
=item * L<frame|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/frame>
=item * L<iframe|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe>
=item * L<meta|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta>
=back
as L<Firefox::Marionette::Link|Firefox::Marionette::Link> objects.
This method is subject to the L<implicit|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.
=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<MacOS|https://en.wikipedia.org/wiki/MacOS>. 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<itself|Firefox::Marionette> 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<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;
}
=head2 minimise
minimises the firefox window. This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 mouse_down
accepts a parameter describing which mouse button the method should apply to (L<left|Firefox::Marionette::Buttons#LEFT>, L<middle|Firefox::Marionette::Buttons#MIDDLE> or L<right|Firefox::Marionette::Buttons#RIGHT>) and returns an action for use in the L<perform|/perform> method that corresponding with a mouse button being depressed.
=head2 mouse_move
accepts a L<element|Firefox::Marionette::Element> parameter, or a C<( x =E<gt> 0, y =E<gt> 0 )> type hash manually describing exactly where to move the mouse to and returns an action for use in the L<perform|/perform> method that corresponding with such a mouse movement, either to the specified co-ordinates or to the middle of the supplied L<element|Firefox::Marionette::Element> parameter. Other parameters that may be passed are listed below;
=over 4
=item * origin - the origin of the C(<x =E<gt> 0, y =E<gt> 0)> co-ordinates. Should be either C<viewport>, C<pointer> or an L<element|Firefox::Marionette::Element>.
=item * duration - Number of milliseconds over which to distribute the move. If not defined, the duration defaults to 0.
=back
This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 mouse_up
accepts a parameter describing which mouse button the method should apply to (L<left|Firefox::Marionette::Buttons#LEFT>, L<middle|Firefox::Marionette::Buttons#MIDDLE> or L<right|Firefox::Marionette::Buttons#RIGHT>) and returns an action for use in the L<perform|/perform> 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<Firefox|https://firefox.org/> binary, rather than the default path.
=item * capabilities - use the supplied L<capabilities|Firefox::Marionette::Capabilities> object, for example to set whether the browser should L<accept insecure certs|Firefox::Marionette::Capabilities#accept_insecure_certs> or whether the browser should use a L<proxy|Firefox::Marionette::Proxy>.
=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<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 L</etc/hosts|https://en.wikipedia.org/wiki//etc/hosts>. NOTE: that this option only works when profile_name/profile is not specified.
=item * console - show the L<browser console|https://developer.mozilla.org/en-US/docs/Tools/Browser_Console/> when the browser is launched. This defaults to "0" (off). See L<CONSOLE LOGGING|/CONSOLE-LOGGING> 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<debug|/debug> method. If this option is not a boolean (0|1), the value will be passed to the L<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.
=item * developer - only allow a L<developer edition|https://www.mozilla.org/en-US/firefox/developer/> to be launched. This defaults to "0" (off).
=item * devtools - begin the session with the L<devtools|https://developer.mozilla.org/en-US/docs/Tools> window opened in a separate window.
=item * geo - setup the browser L<preferences|http://kb.mozillazine.org/About:config> to allow the L<Geolocation API|https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API> to work. If the value for this key is a L<URI|URI> object or a string beginning with '^(?:data|http)', this object will be retrieved using the L<json|/json> method and the response will used to build a L<GeoLocation|Firefox::Mozilla::GeoLocation> object, which will be sent to the L<geo|/geo> method. If the value for this key is a hash, the hash will be used to build a L<GeoLocation|Firefox::Mozilla::GeoLocation> object, which will be sent to the L<geo|/geo> method.
=item * height - set the L<height|http://kb.mozillazine.org/Command_line_arguments#List_of_command_line_arguments_.28incomplete.29> of the initial firefox window
=item * har - begin the session with the L<devtools|https://developer.mozilla.org/en-US/docs/Tools> window opened in a separate window. The L<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 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<ssh|https://man.openbsd.org/ssh.1> to create and automate firefox on the specified host. See L<REMOTE AUTOMATION OF FIREFOX VIA SSH|/REMOTE-AUTOMATION-OF-FIREFOX-VIA-SSH> and L<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 L<ssh-agent|https://man.openbsd.org/ssh-agent>.
=item * implicit - a shortcut to allow directly providing the L<implicit|Firefox::Marionette::Timeout#implicit> 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<kiosk|https://support.mozilla.org/en-US/kb/firefox-enterprise-kiosk-mode> 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<nightly release|https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly> to be launched. This defaults to "0" (off).
=item * port - if the "host" parameter is also set, use L<ssh|https://man.openbsd.org/ssh.1> to create and automate firefox via the specified port. See L<REMOTE AUTOMATION OF FIREFOX VIA SSH|/REMOTE-AUTOMATION-OF-FIREFOX-VIA-SSH> and L<NETWORK ARCHITECTURE|/NETWORK-ARCHITECTURE>.
=item * page_load - a shortcut to allow directly providing the L<page_load|Firefox::Marionette::Timeouts#page_load> 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<profile|Firefox::Marionette::Profile>. NOTE: firefox ignores any changes made to the profile on the disk while it is running, instead, use the L<set_pref|/set_pref> and L<clear_pref|/clear_pref> 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<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 L<Firefox::Marionette::Profile::names()|Firefox::Marionette::Profile#names> method. The following conditions are required to use existing profiles;
=over 8
=item * the preference C<security.webauth.webauthn_enable_softtoken> must be set to C<true> in the profile OR
=item * the C<webauth> 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<set_pref|/set_pref> and L<clear_pref|/clear_pref> methods to make changes while firefox is running.
=item * proxy - this is a shortcut method for setting a L<proxy|Firefox::Marionette::Proxy> using the L<capabilities|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 L<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 L<REMOTE AUTOMATION OF FIREFOX VIA SSH|/REMOTE-AUTOMATION-OF-FIREFOX-VIA-SSH>, L<NETWORK ARCHITECTURE|/NETWORK-ARCHITECTURE> and L<SETTING UP SOCKS SERVERS USING SSH|Firefox::Marionette::Proxy#SETTING-UP-SOCKS-SERVERS-USING-SSH>.
=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<REMOTE AUTOMATION OF FIREFOX VIA SSH|/REMOTE-AUTOMATION-OF-FIREFOX-VIA-SSH> and the --scp-only option in the L<ssh-auth-cmd-marionette|https://metacpan.org/pod/ssh-auth-cmd-marionette> script in this distribution.
=item * script - a shortcut to allow directly providing the L<script|Firefox::Marionette::Timeout#script> 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<await|/await> or L<bye|/bye> methods. This defaults to "1" millisecond.
=item * stealth - stops L<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 L<extension|https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions>, which will automatically switch on the C<addons> parameter for the L<new|/new> method. This is extremely experimental. See L<IMITATING OTHER BROWSERS|/IMITATING-OTHER-BROWSERS> 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 * system_access - firefox L<after version 138|https://bugzilla.mozilla.org/show_bug.cgi?id=1944565> allows disabling system access for javascript. By default, this module will turn on system access.
=item * trust - give a path to a L<root certificate|https://en.wikipedia.org/wiki/Root_certificate> encoded as a L<PEM encoded X.509 certificate|https://datatracker.ietf.org/doc/html/rfc7468#section-5> that will be trusted for this session.
=item * timeouts - a shortcut to allow directly providing a L<timeout|Firefox::Marionette::Timeout> 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<set|/set_pref> to make it harder to be tracked by the L<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;
=item * user - if the "host" parameter is also set, use L<ssh|https://man.openbsd.org/ssh.1> to create and automate firefox with the specified user. See L<REMOTE AUTOMATION OF FIREFOX VIA SSH|/REMOTE-AUTOMATION-OF-FIREFOX-VIA-SSH> and L<NETWORK ARCHITECTURE|/NETWORK-ARCHITECTURE>. The user will default to the current user name. Authentication should be via public keys loaded into the local L<ssh-agent|https://man.openbsd.org/ssh-agent>.
=item * via - specifies a L<proxy jump box|https://man.openbsd.org/ssh_config#ProxyJump> 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<X11 forwarding|https://man.openbsd.org/ssh#X>. See L<X11 FORWARDING WITH FIREFOX|/X11-FORWARDING-WITH-FIREFOX>.
=item * waterfox - only allow a binary that looks like a L<waterfox version|https://www.waterfox.net/> to be launched.
=item * webauthn - a boolean parameter to determine whether or not to L<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.
=item * width - set the L<width|http://kb.mozillazine.org/Command_line_arguments#List_of_command_line_arguments_.28incomplete.29> of the initial firefox window
=back
This method returns a new C<Firefox::Marionette> object, connected to an instance of L<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 L<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 );
...
}
=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<window handle|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);
=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<current version|/browser_version> of firefox is a L<nightly release|https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly> (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<perform|/perform> method that will cause a pause in the chain of actions given to the L<perform|/perform> 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<Firefox 96|https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/96#webdriver_conformance_marionette> 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<shrink_to_fit> should be disabled to make C<scale> 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<paper_sizes|/paper_sizes> 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<File::Temp|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);
...
}
=head2 percentage_visible
accepts an L<element|Firefox::Marionette::Element> as the first parameter and returns the percentage of that L<element|Firefox::Marionette::Element> that is currently visible in the L<viewport|https://developer.mozilla.org/en-US/docs/Glossary/Viewport>. It achieves this by determining the co-ordinates of the L<DOMRect|https://developer.mozilla.org/en-US/docs/Web/API/DOMRect> with a L<getBoundingClientRect|https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect> call and then using L<elementsFromPoint|https://developer.mozilla.org/en-US/docs/Web/API/Document/elementsFromPoint> and L<getComputedStyle|https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle> calls to determine how the percentage of the L<DOMRect|https://developer.mozilla.org/en-US/docs/Web/API/DOMRect> that is visible to the user. The L<getComputedStyle|https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle> call is used to determine the state of the L<visibility|https://developer.mozilla.org/en-US/docs/Web/CSS/visibility> and L<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;
}
}
}
=head2 perform
accepts a list of actions (see L<mouse_up|/mouse_up>, L<mouse_down|/mouse_down>, L<mouse_move|/mouse_move>, L<pause|/pause>, L<key_down|/key_down> and L<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 L<release|/release> method will complete outstanding actions (such as L<mouse_up|/mouse_up> or L<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 L<release|/release> method for an alternative for manually specifying all the L<mouse_up|/mouse_up> and L<key_up|/key_up> 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<element|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 L<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();
=head2 pwd_mgr_lock
Accepts a new L<primary password|https://support.mozilla.org/en-US/kb/use-primary-password-protect-stored-logins> and locks the L<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 L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 pwd_mgr_login
Accepts the L<primary password|https://support.mozilla.org/en-US/kb/use-primary-password-protect-stored-logins> and allows the user to access the L<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 L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 pwd_mgr_logout
Logs the user out of being able to access the L<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 L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 pwd_mgr_needs_login
returns true or false if the L<Password Manager|https://support.mozilla.org/en-US/kb/password-manager-remember-delete-edit-logins> has been locked and needs a L<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);
}
=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<element|Firefox::Marionette::Element> as the first parameter and returns the current L<position and size|Firefox::Marionette::Element::Rect> of the L<element|Firefox::Marionette::Element>
=head2 refresh
refreshes the current page. The browser will wait for the page to completely refresh or the session's L<page_load|Firefox::Marionette::Timeouts#page_load> duration to elapse before returning, which, by default is 5 minutes. This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 release
completes any outstanding actions issued by the L<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();
=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<maximise|Firefox::Marionette::maximise>, then
=item 2 caching the browser's current width and height as the maximum width and height. It
=item 3 then calls L<resizeTo|https://developer.mozilla.org/en-US/docs/Web/API/Window/resizeTo> to resize the window to 0,0
=item 4 wait for the browser to send a L<resize|https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event> 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<itself|Firefox::Marionette> to aid in chaining methods. Otherwise,
=item 8 call L<resizeTo|https://developer.mozilla.org/en-US/docs/Web/API/Window/resizeTo> for the requested width and height
=item 9 wait for the L<resize|https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event> event
=back
This method returns L<itself|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';
}
=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<Firefox::Marionette::DNS|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;
}
=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<itself|Firefox::Marionette> to aid in chaining methods.
=head2 restart
restarts the browser. After the restart, L<capabilities|Firefox::Marionette::Capabilities> should be restored. The same profile settings should be applied, but the current state of the browser (such as the L<uri|/uri> will be reset (like after a normal browser restart). This method is primarily intended for use by the L<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 L<itself|Firefox::Marionette> 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<window|https://developer.mozilla.org/en-US/docs/Web/API/Window> object if C<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.
=item * timeout - A timeout to override the default L<script|Firefox::Marionette::Timeouts#script> timeout, which, by default is 30 seconds.
=back
Returns the result of the javascript function. When a parameter is an L<element|Firefox::Marionette::Element> (such as being returned from a L<find|/find> type operation), the L<script|/script> method will automatically translate that into a javascript object. Likewise, when the result being returned in a L<script|/script> method is an L<element|https://dom.spec.whatwg.org/#concept-element> it will be automatically translated into a L<perl object|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 L<script|Firefox::Marionette::Timeouts#script> timeout, which, by default is 30 seconds.
=head2 selfie
returns a L<File::Temp|File::Temp> object containing a lossless PNG image screenshot. If an L<element|Firefox::Marionette::Element> is passed as a parameter, the screenshot will be restricted to the element.
If an L<element|Firefox::Marionette::Element> is not passed as a parameter and the current L<context|/context> is 'chrome', a screenshot of the current viewport will be returned.
If an L<element|Firefox::Marionette::Element> is not passed as a parameter and the current L<context|/context> is 'content', a screenshot of the current frame will be returned.
The parameters after the L<element|Firefox::Marionette::Element> 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<element|Firefox::Marionette::Element> 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<element|Firefox::Marionette::Element> supplied
=item * highlights - a reference to a list containing L<elements|Firefox::Marionette::Element> to draw a highlight around. Not available in L<Firefox 70|https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/70#WebDriver_conformance_Marionette> onwards.
=back
=head2 scroll
accepts a L<element|Firefox::Marionette::Element> as the first parameter and L<scrolls|https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView> to it. The optional second parameter is the same as for the L<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' });
=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<javascript.enabled|https://support.mozilla.org/en-US/kb/javascript-settings-for-interactive-web-pages#w_for-advanced-users>. This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 set_pref
accepts a L<preference|http://kb.mozillazine.org/About:config> name and the new value to set it to. See the L<get_pref|/get_pref> and L<clear_pref|/clear_pref> methods to get a preference value and to restore it to it's original value. This method returns L<itself|Firefox::Marionette> 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<element|Firefox::Marionette::Element> as a parameter and returns it's L<ShadowRoot|https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot> as a L<shadow root|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 L<FINDING ELEMENTS IN A SHADOW DOM|/FINDING-ELEMENTS-IN-A-SHADOW-DOM> section for how to delve into a L<shadow DOM|https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM>.
=head2 shadowy
accepts an L<element|Firefox::Marionette::Element> as a parameter and returns true if the element has a L<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 L<shadow_root|Firefox::Marionette::Element#shadow_root> method can be called on this element without raising an exception.
=head2 sleep_time_in_ms
accepts a new time to sleep in L<await|/await> or L<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)
=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<json|/json> method for an alternative when dealing with response content types such as application/json and L<html|/html> for an alternative when dealing with html content types. This is a convenience method that wraps the L<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 L<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.
=head2 switch_to_frame
accepts a L<frame|Firefox::Marionette::Element> 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<window handle|Firefox::Marionette::WebWindow> (either the result of L<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');
=head2 tag_name
accepts a L<Firefox::Marionette::Element|Firefox::Marionette::Element> object as the first parameter and returns the relevant tag name. For example 'L<a|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a>' or 'L<input|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input>'.
=head2 text
accepts a L<element|Firefox::Marionette::Element> as the first parameter and returns the text that is contained by that element (if any)
=head2 timeouts
returns the current L<timeouts|Firefox::Marionette::Timeouts> for page loading, searching, and scripts.
=head2 tz
accepts a L<Olson TZ identifier|https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List> as the first parameter. This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
=head2 title
returns the current L<title|https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title> of the window.
=head2 type
accepts an L<element|Firefox::Marionette::Element> as the first parameter and a string as the second parameter. It sends the string to the specified L<element|Firefox::Marionette::Element> in the current page, such as filling out a text box. This method returns L<itself|Firefox::Marionette> 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<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 L<status|Firefox::Marionette::UpdateStatus> 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<install|/install> method. This method returns L<itself|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.
=head2 uri
returns the current L<URI|URI> of current top level browsing context for Desktop. It is equivalent to the javascript C<document.location.href>
=head2 webauthn_authenticator
returns the default L<WebAuthn authenticator|Firefox::Marionette::WebAuthn::Authenticator> created when the L<new|/new> method was called.
=head2 webauthn_credentials
This method accepts an optional L<authenticator|Firefox::Marionette::WebAuthn::Authenticator>, in which case it will return all the L<credentials|Firefox::Marionette::WebAuthn::Credential> attached to this authenticator. If no parameter is supplied, L<credentials|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();
}
=head2 webauthn_set_user_verified
This method accepts a boolean for the L<is_user_verified|Firefox::Marionette::WebAuthn::Authenticator#is_user_verified> field and an optional L<authenticator|Firefox::Marionette::WebAuthn::Authenticator> (the default authenticator will be used otherwise). It sets the L<is_user_verified|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);
=head2 wheel
accepts a L<element|Firefox::Marionette::Element> parameter, or a C<( x =E<gt> 0, y =E<gt> 0 )> type hash manually describing exactly where to move the mouse from and returns an action for use in the L<perform|/perform> method that corresponding with such a wheel action, either to the specified co-ordinates or to the middle of the supplied L<element|Firefox::Marionette::Element> parameter. Other parameters that may be passed are listed below;
=over 4
=item * origin - the origin of the C(<x =E<gt> 0, y =E<gt> 0)> co-ordinates. Should be either C<viewport>, C<pointer> or an L<element|Firefox::Marionette::Element>.
=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<current window's handle|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 L<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?";
}
=head2 window_handles
returns a list of top-level L<browsing contexts|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";
=head2 window_rect
accepts an optional L<position and size|Firefox::Marionette::Window::Rect> as a parameter, sets the current browser window to that position and size and returns the previous L<position, size and state|Firefox::Marionette::Window::Rect> of the browser window. If no parameter is supplied, it returns the current L<position, size and state|Firefox::Marionette::Window::Rect> 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<REMOTE AUTOMATION OF FIREFOX VIA SSH|/REMOTE-AUTOMATION-OF-FIREFOX-VIA-SSH> section for more options.
See L<SETTING UP SOCKS SERVERS USING SSH|Firefox::Marionette::Proxy#SETTING-UP-SOCKS-SERVERS-USING-SSH> for easy proxying via L<ssh|https://man.openbsd.org/ssh>
See L<GEO LOCATION|/GEO-LOCATION> 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<Geolocation API|https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API> can be used by supplying the C<geo> parameter to the L<new|/new> method and then calling the L<geo|/geo> method (from a L<secure context|https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts>).
The L<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 L<geo|/geo> and L<json|/json> methods in providing the desired location to the L<Geolocation API|https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API>.
As mentioned in the L<geo|/geo> method documentation, the L<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 L<tz|/tz> 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<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")]);
=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<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 L<command|https://man.openbsd.org/sshd#command=_command_> option in the L<OpenSSH|https://www.openssh.com/> L<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 L<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 L<ssh-agent|https://man.openbsd.org/ssh-agent> when authenticating.
When using ssh, Firefox::Marionette will attempt to pass the L<TMPDIR|https://en.wikipedia.org/wiki/TMPDIR> environment variable across the ssh connection to make cleanups easier. In order to allow this, the L<AcceptEnv|https://man.openbsd.org/sshd_config#AcceptEnv> setting in the remote L<sshd configuration|https://man.openbsd.org/sshd_config> should be set to allow TMPDIR, which will look like;
AcceptEnv TMPDIR
This module uses L<ControlMaster|https://man.openbsd.org/ssh_config#ControlMaster> functionality when using L<ssh|https://man.openbsd.org/ssh>, for a useful speedup of executing remote commands. Unfortunately, when using ssh to move from a L<cygwin|https://gcc.gnu.org/wiki/SSH_connection_caching>, L<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 L<ControlMaster|https://man.openbsd.org/ssh_config#ControlMaster>, because at this time, Windows L<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 L<NETWORK ARCHITECTURE|/NETWORK-ARCHITECTURE> section has an example of a more complicated network design.
=head1 WEBGL
There are a number of steps to getting L<WebGL|https://en.wikipedia.org/wiki/WebGL> to work correctly;
=over
=item 1. The C<addons> parameter to the L<new|/new> 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<new|/new> method must be set. This is due to L<an existing bug in Firefox|https://bugzilla.mozilla.org/show_bug.cgi?id=1375585>.
=item 3. It can be tricky getting L<WebGL|https://en.wikipedia.org/wiki/WebGL> to work with a L<Xvfb|https://en.wikipedia.org/wiki/Xvfb> instance. L<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.
=back
With all those conditions being met, L<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";
}
=head1 FILE UPLOADS
Uploading files in forms is accomplished by using the L<type|Firefox::Marionette::Element#type> command to enter the full path of the file you want to upload. An example is shown below;
use Firefox::Marionette();
use File::Spec();
use Cwd();
my $firefox = Firefox::Marionette->new();
my $firefox_marionette_directory = Cwd::cwd();
$firefox->go("https://practice.expandtesting.com/upload");
while($firefox->percentage_visible($firefox->find_id("fileSubmit")) < 90) {
sleep 1;
}
$firefox->find_id("fileInput")->type(File::Spec->catfile($firefox_marionette_directory, qw(t 04-uploads.t)));
$firefox->find_id("fileSubmit")->click();
=head1 FINDING ELEMENTS IN A SHADOW DOM
One aspect of L<Web Components|https://developer.mozilla.org/en-US/docs/Web/API/Web_components> is the L<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 L<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 L<xpath|https://bugzilla.mozilla.org/show_bug.cgi?id=1822311> and L<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 L<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.
=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<stealth> parameter of the L<new|/new> method. This method will stop the browser reporting itself as a robot and will also (when combined with the L<agent|/agent> method, change other javascript characteristics to match the L<User Agent|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent> string.
=item * the L<agent|/agent> method, which if supplied a recognisable L<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.
=item * the L<geo|/geo> method, which allows the modification of the L<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 L<NETWORK ARCHITECTURE|/NETWORK-ARCHITECTURE> section for a discussion of different types of proxies that can be used to change your external IP address).
=item * the L<languages|/languages> method, which can change the L<requested languages|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language> for your browser session.
=item * the L<tz|/tz> method, which can change the L<timezone|https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List> for your browser session.
=back
This list of methods may grow.
=head1 WEBSITES THAT BLOCK AUTOMATION
Marionette L<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 L<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 C<stealth> parameter for the L<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 L<ongoing arms race|https://en.wikipedia.org/wiki/Web_scraping#Methods_to_prevent_web_scraping>, and potential L<legal issues|https://en.wikipedia.org/wiki/Web_scraping#Legal_issues> in this area.
=head1 X11 FORWARDING WITH FIREFOX
L<X11 Forwarding|https://man.openbsd.org/ssh#X> allows you to launch a L<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.
=head1 UBUNTU AND FIREFOX DELIVERED VIA SNAP
L<Ubuntu 22.04 LTS|https://ubuntu.com/blog/ubuntu-22-04-lts-whats-new-linux-desktop> is packaging firefox as a L<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;
=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<setup-for-firefox-marionette-build.sh|setup-for-firefox-marionette-build.sh> 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<new_session|/new_session> 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<Capabilities|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.
=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<ulimit> 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<Firefox::Marionette::Element|Firefox::Marionette::Element> 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<capabilities|Firefox::Marionette::Capabilities> do not specify proxy information.
=head1 DEPENDENCIES
Firefox::Marionette requires the following non-core Perl modules
=over
=item *
L<JSON|JSON>
=item *
L<URI|URI>
=item *
L<XML::Parser|XML::Parser>
=item *
L<Time::Local|Time::Local>
=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<go|/go> method to go directly to a URL containing a downloadable file, Firefox can hang. You can work around this by setting the L<page_load_strategy|Firefox::Marionette::Capabilities#page_load_strategy> to C<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 L<download|/download> 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<https://github.com/david-dick/firefox-marionette/issues>
=head1 SEE ALSO
=over
=item *
L<MozRepl|MozRepl>
=item *
L<Selenium::Firefox|Selenium::Firefox>
=item *
L<Firefox::Application|Firefox::Application>
=item *
L<Mozilla::Mechanize|Mozilla::Mechanize>
=item *
L<Gtk2::MozEmbed|Gtk2::MozEmbed>
=back
=head1 AUTHOR
David Dick C<< <ddick@cpan.org> >>
=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<Jan Odvarko|http://www.softwareishard.com/blog/about/> for creating the L<HAR Export Trigger|https://github.com/firefox-devtools/har-export-trigger> extension for Firefox.
Thanks to L<Mike Kaply|https://mike.kaply.com/about/> for his L<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;
=over 4
=item * L<Marionette Protocol|https://firefox-source-docs.mozilla.org/testing/marionette/marionette/index.html>
=item * L<Marionette driver.js|https://hg.mozilla.org/mozilla-central/file/tip/remote/marionette/driver.sys.mjs>
=item * L<about:config|http://kb.mozillazine.org/About:config_entries>
=item * L<nsIPrefService interface|https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPrefService>
=back
=head1 LICENSE AND COPYRIGHT
Copyright (c) 2024, David Dick C<< <ddick@cpan.org> >>. 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<perlartistic/perlartistic>.
The L<Firefox::Marionette::Extension::HarExportTrigger|Firefox::Marionette::Extension::HarExportTrigger> module includes the L<HAR Export Trigger|https://github.com/firefox-devtools/har-export-trigger>
extension which is licensed under the L<Mozilla Public License 2.0|https://www.mozilla.org/en-US/MPL/2.0/>.
=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.