Profile.pm 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. package Selenium::Firefox::Profile;
  2. # ABSTRACT: Use custom profiles with Selenium::Remote::Driver
  3. use strict;
  4. use warnings;
  5. use Archive::Zip qw( :ERROR_CODES );
  6. use Carp qw(croak);
  7. use Cwd qw(abs_path);
  8. use File::Copy qw(copy);
  9. use File::Temp;
  10. use File::Basename qw(dirname);
  11. use IO::Uncompress::Unzip qw(unzip $UnzipError);
  12. use JSON qw/decode_json/;
  13. use MIME::Base64;
  14. use Scalar::Util qw(blessed looks_like_number);
  15. use XML::Simple;
  16. =head1 DESCRIPTION
  17. You can use this module to create a custom Firefox Profile for your
  18. Selenium tests. Currently, you can set browser preferences and add
  19. extensions to the profile before passing it in the constructor for a
  20. new Selenium::Remote::Driver.
  21. =head1 SYNPOSIS
  22. use Selenium::Remote::Driver;
  23. use Selenium::Firefox::Profile;
  24. my $profile = Selenium::Firefox::Profile->new;
  25. $profile->set_preference(
  26. 'browser.startup.homepage' => 'http://www.google.com',
  27. 'browser.cache.disk.capacity' => 358400
  28. );
  29. $profile->set_boolean_preference(
  30. 'browser.shell.checkDefaultBrowser' => 0
  31. );
  32. $profile->add_extension('t/www/redisplay.xpi');
  33. my $driver = Selenium::Remote::Driver->new(
  34. 'firefox_profile' => $profile
  35. );
  36. $driver->get('http://www.google.com');
  37. print $driver->get_title();
  38. =cut
  39. sub new {
  40. my $class = shift;
  41. # TODO: add handling for a pre-existing profile folder passed into
  42. # the constructor
  43. # TODO: accept user prefs, boolean prefs, and extensions in
  44. # constructor
  45. my $self = {
  46. profile_dir => File::Temp->newdir(),
  47. user_prefs => {},
  48. extensions => []
  49. };
  50. bless $self, $class or die "Can't bless $class: $!";
  51. return $self;
  52. }
  53. =method set_preference
  54. Set string and integer preferences on the profile object. You can set
  55. multiple preferences at once. If you need to set a boolean preference,
  56. either use JSON::true/JSON::false, or see C<set_boolean_preference()>.
  57. $profile->set_preference("quoted.integer.pref" => '"20140314220517"');
  58. # user_pref("quoted.integer.pref", "20140314220517");
  59. $profile->set_preference("plain.integer.pref" => 9005);
  60. # user_pref("plain.integer.pref", 9005);
  61. $profile->set_preference("string.pref" => "sample string value");
  62. # user_pref("string.pref", "sample string value");
  63. =cut
  64. sub set_preference {
  65. my ($self, %prefs) = @_;
  66. foreach (keys %prefs) {
  67. my $value = $prefs{$_};
  68. my $clean_value = '';
  69. if ( JSON::is_bool($value) ) {
  70. $self->set_boolean_preference($_, $value );
  71. next;
  72. }
  73. elsif ($value =~ /^(['"]).*\1$/ or looks_like_number($value)) {
  74. # plain integers: 0, 1, 32768, or integers wrapped in strings:
  75. # "0", "1", "20140204". in either case, there's nothing for us
  76. # to do.
  77. $clean_value = $value;
  78. }
  79. else {
  80. # otherwise it's hopefully a string that we'll need to
  81. # quote on our own
  82. $clean_value = '"' . $value . '"';
  83. }
  84. $self->{user_prefs}->{$_} = $clean_value;
  85. }
  86. }
  87. =method set_boolean_preference
  88. Set preferences that require boolean values of 'true' or 'false'. You
  89. can set multiple preferences at once. For string or integer
  90. preferences, use C<set_preference()>.
  91. $profile->set_boolean_preference("false.pref" => 0);
  92. # user_pref("false.pref", false);
  93. $profile->set_boolean_preference("true.pref" => 1);
  94. # user_pref("true.pref", true);
  95. =cut
  96. sub set_boolean_preference {
  97. my ($self, %prefs) = @_;
  98. foreach (keys %prefs) {
  99. my $value = $prefs{$_};
  100. $self->{user_prefs}->{$_} = $value ? 'true' : 'false';
  101. }
  102. }
  103. =method get_preference
  104. Retrieve the computed value of a preference. Strings will be double
  105. quoted and boolean values will be single quoted as "true" or "false"
  106. accordingly.
  107. $profile->set_boolean_preference("true.pref" => 1);
  108. print $profile->get_preference("true.pref") # true
  109. $profile->set_preference("string.pref" => "an extra set of quotes");
  110. print $profile->get_preference("string.pref") # "an extra set of quotes"
  111. =cut
  112. sub get_preference {
  113. my ($self, $pref) = @_;
  114. return $self->{user_prefs}->{$pref};
  115. }
  116. =method add_extension
  117. Add an existing C<.xpi> to the profile by providing its path. This
  118. only works with packaged C<.xpi> files, not plain/un-packed extension
  119. directories.
  120. $profile->add_extension('t/www/redisplay.xpi');
  121. =cut
  122. sub add_extension {
  123. my ($self, $xpi) = @_;
  124. croak 'File not found: ' . $xpi unless -e $xpi;
  125. my $xpi_abs_path = abs_path($xpi);
  126. croak '$xpi_abs_path: extensions must be in .xpi format' unless $xpi_abs_path =~ /\.xpi$/;
  127. push (@{$self->{extensions}}, $xpi_abs_path);
  128. }
  129. =method add_webdriver
  130. Primarily for internal use, we add the webdriver extension to the
  131. current Firefox profile.
  132. =cut
  133. sub add_webdriver {
  134. my ($self, $port) = @_;
  135. my $this_dir = dirname(abs_path(__FILE__));
  136. my $webdriver_extension = $this_dir . '/webdriver.xpi';
  137. my $default_prefs_filename = $this_dir . '/webdriver_prefs.json';
  138. my $json;
  139. {
  140. local $/;
  141. open (my $fh, '<', $default_prefs_filename);
  142. $json = <$fh>;
  143. close ($fh);
  144. }
  145. my $webdriver_prefs = decode_json($json);
  146. # TODO: Let the user's mutable preferences persist instead of
  147. # overwriting them here.
  148. $self->set_preference(%{ $webdriver_prefs->{mutable} });
  149. $self->set_preference(%{ $webdriver_prefs->{frozen} });
  150. $self->add_extension($webdriver_extension);
  151. $self->set_preference('webdriver_firefox_port', $port);
  152. }
  153. sub _encode {
  154. my $self = shift;
  155. # The remote webdriver accepts the Firefox profile as a base64
  156. # encoded zip file
  157. $self->_layout_on_disk();
  158. my $zip = Archive::Zip->new();
  159. my $dir_member = $zip->addTree( $self->{profile_dir} );
  160. my $string = "";
  161. open (my $fh, ">", \$string);
  162. binmode($fh);
  163. unless ( $zip->writeToFileHandle($fh) == AZ_OK ) {
  164. die 'write error';
  165. }
  166. return encode_base64($string);
  167. }
  168. sub _layout_on_disk {
  169. my $self = shift;
  170. $self->_write_preferences();
  171. $self->_install_extensions();
  172. return $self->{profile_dir};
  173. }
  174. sub _write_preferences {
  175. my $self = shift;
  176. my $userjs = $self->{profile_dir} . "/user.js";
  177. open (my $fh, ">>", $userjs)
  178. or die "Cannot open $userjs for writing preferences: $!";
  179. foreach (keys %{$self->{user_prefs}}) {
  180. print $fh 'user_pref("' . $_ . '", ' . $self->get_preference($_) . ');' . "\n";
  181. }
  182. close ($fh);
  183. }
  184. sub _install_extensions {
  185. my $self = shift;
  186. my $extension_dir = $self->{profile_dir} . "/extensions/";
  187. mkdir $extension_dir unless -d $extension_dir;
  188. # TODO: handle extensions that need to be unpacked
  189. foreach my $xpi (@{$self->{extensions}}) {
  190. # For Firefox to recognize the extension, we have to put the
  191. # .xpi in the /extensions/ folder and change the filename to
  192. # its id, which is found in the install.rdf in the root of the
  193. # zip.
  194. my $fh;
  195. unzip $xpi => \$fh, Name => "install.rdf"
  196. or die "unzip failed: $UnzipError\n";
  197. my $rdf = XMLin($fh);
  198. my $name = $rdf->{Description}->{'em:id'};
  199. my $xpi_dest = $extension_dir . $name . ".xpi";
  200. copy($xpi, $xpi_dest)
  201. or croak "Error copying $_ to $xpi_dest : $!";
  202. }
  203. }
  204. 1;
  205. __END__
  206. =head1 SEE ALSO
  207. http://kb.mozillazine.org/About:config_entries
  208. https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/A_brief_guide_to_Mozilla_preferences