1
0

Profile.pm 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. package Selenium::Firefox::Profile;
  2. # ABSTRACT: Use custom profiles with Selenium::Remote::Driver
  3. # TODO: convert this to Moo!
  4. use strict;
  5. use warnings;
  6. use Archive::Zip qw( :ERROR_CODES );
  7. use Carp qw(croak);
  8. use Cwd qw(abs_path);
  9. use File::Copy qw(copy);
  10. use File::Temp;
  11. use File::Basename qw(dirname);
  12. use IO::Uncompress::Unzip 2.030 qw($UnzipError);
  13. use JSON qw(decode_json);
  14. use MIME::Base64;
  15. use Scalar::Util qw(blessed looks_like_number);
  16. use XML::Simple;
  17. =head1 DESCRIPTION
  18. You can use this module to create a custom Firefox Profile for your
  19. Selenium tests. Currently, you can set browser preferences and add
  20. extensions to the profile before passing it in the constructor for a
  21. new L<Selenium::Remote::Driver> or L<Selenium::Firefox>.
  22. =head1 SYNPOSIS
  23. use Selenium::Remote::Driver;
  24. use Selenium::Firefox::Profile;
  25. my $profile = Selenium::Firefox::Profile->new;
  26. $profile->set_preference(
  27. 'browser.startup.homepage' => 'http://www.google.com',
  28. 'browser.cache.disk.capacity' => 358400
  29. );
  30. $profile->set_boolean_preference(
  31. 'browser.shell.checkDefaultBrowser' => 0
  32. );
  33. $profile->add_extension('t/www/redisplay.xpi');
  34. my $driver = Selenium::Remote::Driver->new(
  35. 'firefox_profile' => $profile
  36. );
  37. $driver->get('http://www.google.com');
  38. print $driver->get_title();
  39. =cut
  40. =head1 CONSTRUCTOR
  41. =head2 new (%args)
  42. profile_dir - <string> directory to look for the firefox profile. Defaults to a Tempdir.
  43. =cut
  44. sub new {
  45. my $class = shift;
  46. my %args = @_;
  47. my $profile_dir;
  48. if ( $args{profile_dir} && -d $args{profile_dir} ) {
  49. $profile_dir = $args{profile_dir};
  50. }
  51. else {
  52. $profile_dir = File::Temp->newdir();
  53. }
  54. # TODO: accept user prefs, boolean prefs, and extensions in
  55. # constructor
  56. my $self = {
  57. profile_dir => $profile_dir,
  58. user_prefs => {},
  59. extensions => []
  60. };
  61. bless $self, $class or die "Can't bless $class: $!";
  62. return $self;
  63. }
  64. =head1 METHODS
  65. =head2 set_preference
  66. Set string and integer preferences on the profile object. You can set
  67. multiple preferences at once. If you need to set a boolean preference,
  68. either use JSON::true/JSON::false, or see C<set_boolean_preference()>.
  69. $profile->set_preference("quoted.integer.pref" => '"20140314220517"');
  70. # user_pref("quoted.integer.pref", "20140314220517");
  71. $profile->set_preference("plain.integer.pref" => 9005);
  72. # user_pref("plain.integer.pref", 9005);
  73. $profile->set_preference("string.pref" => "sample string value");
  74. # user_pref("string.pref", "sample string value");
  75. =cut
  76. sub set_preference {
  77. my ($self, %prefs) = @_;
  78. foreach (keys %prefs) {
  79. my $value = $prefs{$_};
  80. my $clean_value = '';
  81. if ( JSON::is_bool($value) ) {
  82. $self->set_boolean_preference($_, $value );
  83. next;
  84. }
  85. elsif ($value =~ /^(['"]).*\1$/ or looks_like_number($value)) {
  86. # plain integers: 0, 1, 32768, or integers wrapped in strings:
  87. # "0", "1", "20140204". in either case, there's nothing for us
  88. # to do.
  89. $clean_value = $value;
  90. }
  91. else {
  92. # otherwise it's hopefully a string that we'll need to
  93. # quote on our own
  94. $clean_value = '"' . $value . '"';
  95. }
  96. $self->{user_prefs}->{$_} = $clean_value;
  97. }
  98. }
  99. =head2 set_boolean_preference
  100. Set preferences that require boolean values of 'true' or 'false'. You
  101. can set multiple preferences at once. For string or integer
  102. preferences, use C<set_preference()>.
  103. $profile->set_boolean_preference("false.pref" => 0);
  104. # user_pref("false.pref", false);
  105. $profile->set_boolean_preference("true.pref" => 1);
  106. # user_pref("true.pref", true);
  107. =cut
  108. sub set_boolean_preference {
  109. my ($self, %prefs) = @_;
  110. foreach (keys %prefs) {
  111. my $value = $prefs{$_};
  112. $self->{user_prefs}->{$_} = $value ? 'true' : 'false';
  113. }
  114. }
  115. =head2 get_preference
  116. Retrieve the computed value of a preference. Strings will be double
  117. quoted and boolean values will be single quoted as "true" or "false"
  118. accordingly.
  119. $profile->set_boolean_preference("true.pref" => 1);
  120. print $profile->get_preference("true.pref") # true
  121. $profile->set_preference("string.pref" => "an extra set of quotes");
  122. print $profile->get_preference("string.pref") # "an extra set of quotes"
  123. =cut
  124. sub get_preference {
  125. my ($self, $pref) = @_;
  126. return $self->{user_prefs}->{$pref};
  127. }
  128. =head2 add_extension
  129. Add an existing C<.xpi> to the profile by providing its path. This
  130. only works with packaged C<.xpi> files, not plain/un-packed extension
  131. directories.
  132. $profile->add_extension('t/www/redisplay.xpi');
  133. =cut
  134. sub add_extension {
  135. my ($self, $xpi) = @_;
  136. croak 'File not found: ' . $xpi unless -e $xpi;
  137. my $xpi_abs_path = abs_path($xpi);
  138. croak '$xpi_abs_path: extensions must be in .xpi format' unless $xpi_abs_path =~ /\.xpi$/;
  139. push (@{$self->{extensions}}, $xpi_abs_path);
  140. }
  141. =head2 add_webdriver
  142. Primarily for internal use, we set the appropriate firefox preferences
  143. for a new geckodriver session.
  144. =cut
  145. sub add_webdriver {
  146. my ($self, $port, $is_marionette) = @_;
  147. my $prefs = $self->_load_prefs;
  148. my $current_user_prefs = $self->{user_prefs};
  149. $self->set_preference(
  150. %{ $prefs->{mutable} },
  151. # having the user prefs here allows them to overwrite the
  152. # mutable loaded prefs
  153. %{ $current_user_prefs },
  154. # but the frozen ones cannot be overwritten
  155. %{ $prefs->{frozen} },
  156. 'webdriver_firefox_port' => $port
  157. );
  158. if (! $is_marionette) {
  159. $self->_add_webdriver_xpi;
  160. }
  161. return $self;
  162. }
  163. sub _load_prefs {
  164. # The appropriate webdriver preferences are stored in an adjacent
  165. # JSON file; it's useful things like disabling default browser
  166. # checks and setting an empty single page as the start up tab
  167. # configuration. Unfortunately, these change with each version of
  168. # webdriver.
  169. my $this_dir = dirname(abs_path(__FILE__));
  170. my $default_prefs_filename = $this_dir . '/webdriver_prefs.json';
  171. my $json;
  172. {
  173. local $/;
  174. open (my $fh, '<', $default_prefs_filename);
  175. $json = <$fh>;
  176. close ($fh);
  177. }
  178. my $prefs = decode_json($json);
  179. return $prefs;
  180. }
  181. =head2 add_webdriver_xpi
  182. Primarily for internal use. This adds the fxgoogle .xpi that is used
  183. for webdriver communication in FF47 and older. For FF48 and newer, the
  184. old method using an extension to orchestrate the webdriver
  185. communication with the Firefox browser has been obsoleted by the
  186. introduction of C<geckodriver>.
  187. =cut
  188. sub _add_webdriver_xpi {
  189. my ($self) = @_;
  190. my $this_dir = dirname(abs_path(__FILE__));
  191. my $webdriver_extension = $this_dir . '/webdriver.xpi';
  192. $self->add_extension($webdriver_extension);
  193. }
  194. =head2 add_marionette
  195. Primarily for internal use, configure Marionette to the
  196. current Firefox profile.
  197. =cut
  198. sub add_marionette {
  199. my ($self, $port) = @_;
  200. return if !$port;
  201. $self->set_preference('marionette.defaultPrefs.port', $port);
  202. }
  203. sub _encode {
  204. my $self = shift;
  205. # The remote webdriver accepts the Firefox profile as a base64
  206. # encoded zip file
  207. $self->_layout_on_disk();
  208. my $zip = Archive::Zip->new();
  209. $zip->addTree( $self->{profile_dir} );
  210. my $string = "";
  211. open (my $fh, ">", \$string);
  212. binmode($fh);
  213. unless ( $zip->writeToFileHandle($fh) == AZ_OK ) {
  214. die 'write error';
  215. }
  216. return encode_base64($string, '');
  217. }
  218. sub _layout_on_disk {
  219. my $self = shift;
  220. $self->_write_preferences();
  221. $self->_install_extensions();
  222. return $self->{profile_dir};
  223. }
  224. sub _write_preferences {
  225. my $self = shift;
  226. my $userjs = $self->{profile_dir} . "/user.js";
  227. open (my $fh, ">>", $userjs)
  228. or die "Cannot open $userjs for writing preferences: $!";
  229. foreach (keys %{$self->{user_prefs}}) {
  230. print $fh 'user_pref("' . $_ . '", ' . $self->get_preference($_) . ');' . "\n";
  231. }
  232. close ($fh);
  233. }
  234. sub _install_extensions {
  235. my $self = shift;
  236. my $extension_dir = $self->{profile_dir} . "/extensions/";
  237. mkdir $extension_dir unless -d $extension_dir;
  238. # TODO: handle extensions that need to be unpacked
  239. foreach my $xpi (@{$self->{extensions}}) {
  240. # For Firefox to recognize the extension, we have to put the
  241. # .xpi in the /extensions/ folder and change the filename to
  242. # its id, which is found in the install.rdf in the root of the
  243. # zip.
  244. my $rdf_string = $self->_extract_install_rdf($xpi);
  245. my $rdf = XMLin($rdf_string);
  246. my $name = $rdf->{Description}->{'em:id'};
  247. my $xpi_dest = $extension_dir . $name . ".xpi";
  248. copy($xpi, $xpi_dest)
  249. or croak "Error copying $_ to $xpi_dest : $!";
  250. }
  251. }
  252. sub _extract_install_rdf {
  253. my ($self, $xpi) = @_;
  254. my $unzipped = IO::Uncompress::Unzip->new($xpi)
  255. or die "Cannot unzip $xpi: $UnzipError";
  256. my $install_rdf = '';
  257. while ($unzipped->nextStream) {
  258. my $filename = $unzipped->getHeaderInfo->{Name};
  259. if ($filename eq 'install.rdf') {
  260. my $buffer;
  261. while ((my $status = $unzipped->read($buffer)) > 0) {
  262. $install_rdf .= $buffer;
  263. }
  264. return $install_rdf;
  265. }
  266. }
  267. croak 'Invalid Firefox extension: could not find install.rdf in the .XPI at: ' . $xpi
  268. }
  269. 1;
  270. __END__
  271. =head1 SEE ALSO
  272. http://kb.mozillazine.org/About:config_entries
  273. https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/A_brief_guide_to_Mozilla_preferences