Profile.pm 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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 Selenium::Remote::Driver.
  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. sub new {
  41. my $class = shift;
  42. my %args = @_;
  43. my $profile_dir;
  44. if ( $args{profile_dir} && -d $args{profile_dir} ) {
  45. $profile_dir = $args{profile_dir};
  46. }
  47. else {
  48. $profile_dir = File::Temp->newdir();
  49. }
  50. # TODO: accept user prefs, boolean prefs, and extensions in
  51. # constructor
  52. my $self = {
  53. profile_dir => $profile_dir,
  54. user_prefs => {},
  55. extensions => []
  56. };
  57. bless $self, $class or die "Can't bless $class: $!";
  58. return $self;
  59. }
  60. =method set_preference
  61. Set string and integer preferences on the profile object. You can set
  62. multiple preferences at once. If you need to set a boolean preference,
  63. either use JSON::true/JSON::false, or see C<set_boolean_preference()>.
  64. $profile->set_preference("quoted.integer.pref" => '"20140314220517"');
  65. # user_pref("quoted.integer.pref", "20140314220517");
  66. $profile->set_preference("plain.integer.pref" => 9005);
  67. # user_pref("plain.integer.pref", 9005);
  68. $profile->set_preference("string.pref" => "sample string value");
  69. # user_pref("string.pref", "sample string value");
  70. =cut
  71. sub set_preference {
  72. my ($self, %prefs) = @_;
  73. foreach (keys %prefs) {
  74. my $value = $prefs{$_};
  75. my $clean_value = '';
  76. if ( JSON::is_bool($value) ) {
  77. $self->set_boolean_preference($_, $value );
  78. next;
  79. }
  80. elsif ($value =~ /^(['"]).*\1$/ or looks_like_number($value)) {
  81. # plain integers: 0, 1, 32768, or integers wrapped in strings:
  82. # "0", "1", "20140204". in either case, there's nothing for us
  83. # to do.
  84. $clean_value = $value;
  85. }
  86. else {
  87. # otherwise it's hopefully a string that we'll need to
  88. # quote on our own
  89. $clean_value = '"' . $value . '"';
  90. }
  91. $self->{user_prefs}->{$_} = $clean_value;
  92. }
  93. }
  94. =method set_boolean_preference
  95. Set preferences that require boolean values of 'true' or 'false'. You
  96. can set multiple preferences at once. For string or integer
  97. preferences, use C<set_preference()>.
  98. $profile->set_boolean_preference("false.pref" => 0);
  99. # user_pref("false.pref", false);
  100. $profile->set_boolean_preference("true.pref" => 1);
  101. # user_pref("true.pref", true);
  102. =cut
  103. sub set_boolean_preference {
  104. my ($self, %prefs) = @_;
  105. foreach (keys %prefs) {
  106. my $value = $prefs{$_};
  107. $self->{user_prefs}->{$_} = $value ? 'true' : 'false';
  108. }
  109. }
  110. =method get_preference
  111. Retrieve the computed value of a preference. Strings will be double
  112. quoted and boolean values will be single quoted as "true" or "false"
  113. accordingly.
  114. $profile->set_boolean_preference("true.pref" => 1);
  115. print $profile->get_preference("true.pref") # true
  116. $profile->set_preference("string.pref" => "an extra set of quotes");
  117. print $profile->get_preference("string.pref") # "an extra set of quotes"
  118. =cut
  119. sub get_preference {
  120. my ($self, $pref) = @_;
  121. return $self->{user_prefs}->{$pref};
  122. }
  123. =method add_extension
  124. Add an existing C<.xpi> to the profile by providing its path. This
  125. only works with packaged C<.xpi> files, not plain/un-packed extension
  126. directories.
  127. $profile->add_extension('t/www/redisplay.xpi');
  128. =cut
  129. sub add_extension {
  130. my ($self, $xpi) = @_;
  131. croak 'File not found: ' . $xpi unless -e $xpi;
  132. my $xpi_abs_path = abs_path($xpi);
  133. croak '$xpi_abs_path: extensions must be in .xpi format' unless $xpi_abs_path =~ /\.xpi$/;
  134. push (@{$self->{extensions}}, $xpi_abs_path);
  135. }
  136. =method add_webdriver
  137. Primarily for internal use, we add the webdriver extension to the
  138. current Firefox profile.
  139. =cut
  140. sub add_webdriver {
  141. my ($self, $port) = @_;
  142. my $this_dir = dirname(abs_path(__FILE__));
  143. my $webdriver_extension = $this_dir . '/webdriver.xpi';
  144. my $default_prefs_filename = $this_dir . '/webdriver_prefs.json';
  145. my $json;
  146. {
  147. local $/;
  148. open (my $fh, '<', $default_prefs_filename);
  149. $json = <$fh>;
  150. close ($fh);
  151. }
  152. my $webdriver_prefs = decode_json($json);
  153. my $current_user_prefs = $self->{user_prefs};
  154. $self->set_preference(
  155. %{ $webdriver_prefs->{mutable} },
  156. %{ $current_user_prefs }
  157. );
  158. $self->set_preference(%{ $webdriver_prefs->{frozen} });
  159. $self->add_extension($webdriver_extension);
  160. $self->set_preference('webdriver_firefox_port', $port);
  161. }
  162. =method add_marionette
  163. Primarily for internal use, configure Marionette to the
  164. current Firefox profile.
  165. =cut
  166. sub add_marionette {
  167. my ($self, $port) = @_;
  168. return if !$port;
  169. $self->set_preference('marionette.defaultPrefs.port', $port);
  170. }
  171. sub _encode {
  172. my $self = shift;
  173. # The remote webdriver accepts the Firefox profile as a base64
  174. # encoded zip file
  175. $self->_layout_on_disk();
  176. my $zip = Archive::Zip->new();
  177. my $dir_member = $zip->addTree( $self->{profile_dir} );
  178. my $string = "";
  179. open (my $fh, ">", \$string);
  180. binmode($fh);
  181. unless ( $zip->writeToFileHandle($fh) == AZ_OK ) {
  182. die 'write error';
  183. }
  184. return encode_base64($string, '');
  185. }
  186. sub _layout_on_disk {
  187. my $self = shift;
  188. $self->_write_preferences();
  189. $self->_install_extensions();
  190. return $self->{profile_dir};
  191. }
  192. sub _write_preferences {
  193. my $self = shift;
  194. my $userjs = $self->{profile_dir} . "/user.js";
  195. open (my $fh, ">>", $userjs)
  196. or die "Cannot open $userjs for writing preferences: $!";
  197. foreach (keys %{$self->{user_prefs}}) {
  198. print $fh 'user_pref("' . $_ . '", ' . $self->get_preference($_) . ');' . "\n";
  199. }
  200. close ($fh);
  201. }
  202. sub _install_extensions {
  203. my $self = shift;
  204. my $extension_dir = $self->{profile_dir} . "/extensions/";
  205. mkdir $extension_dir unless -d $extension_dir;
  206. # TODO: handle extensions that need to be unpacked
  207. foreach my $xpi (@{$self->{extensions}}) {
  208. # For Firefox to recognize the extension, we have to put the
  209. # .xpi in the /extensions/ folder and change the filename to
  210. # its id, which is found in the install.rdf in the root of the
  211. # zip.
  212. my $rdf_string = $self->_extract_install_rdf($xpi);
  213. my $rdf = XMLin($rdf_string);
  214. my $name = $rdf->{Description}->{'em:id'};
  215. my $xpi_dest = $extension_dir . $name . ".xpi";
  216. copy($xpi, $xpi_dest)
  217. or croak "Error copying $_ to $xpi_dest : $!";
  218. }
  219. }
  220. sub _extract_install_rdf {
  221. my ($self, $xpi) = @_;
  222. my $unzipped = IO::Uncompress::Unzip->new($xpi)
  223. or die "Cannot unzip $xpi: $UnzipError";
  224. my $install_rdf = '';
  225. while ($unzipped->nextStream) {
  226. my $filename = $unzipped->getHeaderInfo->{Name};
  227. if ($filename eq 'install.rdf') {
  228. my $buffer;
  229. while ((my $status = $unzipped->read($buffer)) > 0) {
  230. $install_rdf .= $buffer;
  231. }
  232. return $install_rdf;
  233. }
  234. }
  235. croak 'Invalid Firefox extension: could not find install.rdf in the .XPI at: ' . $xpi
  236. }
  237. 1;
  238. __END__
  239. =head1 SEE ALSO
  240. http://kb.mozillazine.org/About:config_entries
  241. https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/A_brief_guide_to_Mozilla_preferences